Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,10 @@ export class CreateOperation extends MutationOperation {
this.projectionOperations.push(...operations);
}

/** Post subqueries */
public getAuthorizationSubqueries(_context: QueryASTContext): Cypher.Clause[] {
const nestedContext = this.nestedContext;

if (!nestedContext) {
if (!nestedContext || !nestedContext.hasTarget()) {
throw new Error(
"Error parsing query, nested context not available, need to call transpile first. Please contact support"
);
Expand Down Expand Up @@ -136,11 +135,11 @@ export class CreateOperation extends MutationOperation {

const createClause = new Cypher.Create(createPattern);

const setParams = Array.from(this.inputFields.values()).flatMap((input) => {
const setParams = this.inputFields.flatMap((input) => {
return input.getSetParams(nestedContext);
});

const mutationSubqueries = Array.from(this.inputFields.values()).flatMap((input) => {
const mutationSubqueries = this.inputFields.flatMap((input) => {
return input.getSubqueries(nestedContext);
});

Expand Down Expand Up @@ -171,19 +170,15 @@ export class CreateOperation extends MutationOperation {
return { projectionExpr: nestedContext.target, clauses: [clauses] };
}

private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] {
const { selections, subqueries, predicates, validations } = this.transpileAuthClauses(context);
const predicate = Cypher.and(...predicates);
const lastSelection = selections[selections.length - 1];
private getAuthorizationClauses(context: QueryASTContext<Cypher.Node>): Cypher.Clause[] {
const { selections, subqueries, validations } = this.transpileAuthClauses(context);

if (!predicates.length && !validations.length) {
if (!validations.length) {
return [];
} else {
if (lastSelection) {
lastSelection.where(predicate);
return [...subqueries, new Cypher.With("*"), ...selections, ...validations];
}
return [...subqueries, new Cypher.With("*").where(predicate), ...selections, ...validations];
return [
Cypher.utils.concat(...subqueries.map((sq) => new Cypher.Call(sq, "*")), ...selections, ...validations),
];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class TopLevelCreateMutationOperation extends Operation {
// The response fields in the mutation, currently only READ operations are supported in the MutationResponse
private readonly projectionOperations: OperationField[];

private readonly createOperations: CreateOperation[] = [];
private readonly topLevelCreateOperations: CreateOperation[] = [];

constructor({
createOperations,
Expand All @@ -42,36 +42,50 @@ export class TopLevelCreateMutationOperation extends Operation {
projectionOperations: OperationField[];
}) {
super();
this.createOperations = createOperations;
this.topLevelCreateOperations = createOperations;
this.projectionOperations = projectionOperations;
}

public getChildren(): QueryASTNode[] {
return filterTruthy([...this.createOperations, ...this.projectionOperations]);
return filterTruthy([...this.topLevelCreateOperations, ...this.projectionOperations]);
}

public transpile(context: QueryASTContext): OperationTranspileResult {
if (!context.hasTarget()) {
throw new Error("No parent node found!");
}
const subqueries = this.createOperations.map((field) => {
const { clauses, projectionExpr } = field.transpile(context);
const operationQueries = this.topLevelCreateOperations.map((createOperation) => {
const { clauses, projectionExpr } = createOperation.transpile(context);

const authSubqueries = this.getAuthorizationSubqueriesForCreateOperation(createOperation, context);

return Cypher.utils.concat(
...clauses,
...field.getAuthorizationSubqueries(context),
...authSubqueries,
new Cypher.Return([projectionExpr, context.returnVariable])
);
});

const unionStatement = new Cypher.Call(new Cypher.Union(...subqueries));
const unionStatement = new Cypher.Call(new Cypher.Union(...operationQueries));
const projection: Cypher.Clause = this.getProjectionClause(context);
return {
projectionExpr: context.returnVariable,
clauses: [unionStatement, projection],
};
}

private getAuthorizationSubqueriesForCreateOperation(
operation: CreateOperation,
context: QueryASTContext<Cypher.Node>
): Cypher.Clause[] {
const authSubqueries = operation.getAuthorizationSubqueries(context).map((sq) => new Cypher.Call(sq, "*"));
if (authSubqueries.length > 0) {
return [new Cypher.With("*"), ...authSubqueries];
}

return [];
}

private getProjectionClause(context: QueryASTContext<Cypher.Node>): Cypher.Clause {
const projectionOperation = this.projectionOperations[0]; // TODO: multiple projection operations not supported

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,10 @@ describe("authorization-with-aggregation-filter", () => {
expect((gqlResult.errors as any[])[0].message).toBe("Forbidden");
});

test("should authorize update operations on post with exactly two likes", async () => {
// Test disabled due to flakyness. Enable once `validatePredicate` has been removed from update operations.
// The flakyness is caused by the `AND` operation, that doesn't guarantee shortcircuit of each predicate
// eslint-disable-next-line jest/no-disabled-tests
test.skip("should authorize update operations on post with exactly two likes", async () => {
const typeDefs = /* GraphQL */ `
type ${User} @node {
id: ID!
Expand Down
142 changes: 142 additions & 0 deletions packages/graphql/tests/integration/issues/6797.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { UniqueType } from "../../utils/graphql-types";
import { TestHelper } from "../../utils/tests-helper";

describe("https://github.com/neo4j/graphql/issues/6797", () => {
const testHelper = new TestHelper();
let typeDefs: string;
const secret = "sssh";

let Group: UniqueType;
let Invitee: UniqueType;

beforeAll(async () => {
Group = testHelper.createUniqueType("Group");
Invitee = testHelper.createUniqueType("Invitee");

typeDefs = /* GraphQL */ `
type ${Group} @node {
id: ID! @id
name: String!
invitees: [${Invitee}!]! @relationship(type: "INVITED_TO", direction: IN, aggregate: true)
}

enum InviteeStatus {
PENDING
ACCEPTED
}

type ${Invitee}
@node
@authorization(
validate: [
{
operations: [CREATE]
where: { node: { group_ALL: { inviteesAggregate: { count_LT: 5 } } } }
}
]
) {
id: ID! @id
group: [${Group}!]! @relationship(type: "INVITED_TO", direction: OUT)
email: String!
status: InviteeStatus! @default(value: PENDING)
}
`;

await testHelper.initNeo4jGraphQL({
typeDefs,
features: {
authorization: {
key: secret,
},
},
});
});

afterAll(async () => {
await testHelper.close();
});

test("create and connect invitees to groups", async () => {
await testHelper.executeCypher(`
CREATE (:${Group} { id: "an-id", name: "groupymcgroupface" });
`);

const mutation = /* GraphQL */ `
mutation {
${Group.operations.create}(
input: [
{
name: "My Name"
invitees: {
create: [
{
node: {
email: "an email"
group: { connect: [{ where: { node: { id_EQ: "an-id" } } }] }
}
}
]
}
}
]
) {
${Group.plural} {
invitees {
email
group {
id
}
}
}
}
}
`;

const token = testHelper.createBearerToken(secret);
const queryResult = await testHelper.executeGraphQLWithToken(mutation, token);

expect(queryResult.errors).toBeUndefined();
expect(queryResult.data).toEqual({
[Group.operations.create]: {
[Group.plural]: [
{
invitees: [
{
email: "an email",
group: expect.toIncludeSameMembers([
{
id: "an-id",
},
{
id: expect.toBeString(),
},
]),
},
],
},
],
},
});

await testHelper.expectRelationship(Invitee, Group, "INVITED_TO").count(2);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,9 @@ describe("Cypher Auth Where with Roles", () => {
} AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
WITH *
CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])
CALL (*) {
CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
RETURN this0 AS this
}
WITH this
Expand Down Expand Up @@ -973,7 +975,9 @@ describe("Cypher Auth Where with Roles", () => {
} AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
WITH *
CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param11 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])
CALL (*) {
CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param11 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
RETURN this0 AS this
}
WITH this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,18 @@ describe("Cypher Auth Allow", () => {
SET
this1.id = $param3
WITH *
CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])
WITH *
CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS {
MATCH (this1)<-[:HAS_CONTENT]-(this5:User)
WHERE ($jwt.sub IS NOT NULL AND this5.id = $jwt.sub)
}), \\"@neo4j/graphql/FORBIDDEN\\", [0])
WITH *
CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])
CALL (*) {
CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
CALL (*) {
CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS {
MATCH (this1)<-[:HAS_CONTENT]-(this5:User)
WHERE ($jwt.sub IS NOT NULL AND this5.id = $jwt.sub)
}), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
CALL (*) {
CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
RETURN this0 AS this
}
WITH this
Expand Down Expand Up @@ -221,9 +225,12 @@ describe("Cypher Auth Allow", () => {
SET
this1.id = $param3
WITH *
CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])
WITH *
CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])
CALL (*) {
CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
CALL (*) {
CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])
}
RETURN this0 AS this
}
WITH this
Expand Down
Loading
Loading