Skip to content

Commit

Permalink
Merge pull request #5072 from neo4j/fix/5066
Browse files Browse the repository at this point in the history
Fix/5066
  • Loading branch information
angrykoala authored May 7, 2024
2 parents 1dfd76f + aec402e commit 4c09310
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-owls-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": patch
---

Fix authorization with unions
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
import Cypher from "@neo4j/cypher-builder";
import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter";
import { hasTarget } from "../../../utils/context-has-target";
import { wrapSubqueriesInCypherCalls } from "../../../utils/wrap-subquery-in-calls";
import type { QueryASTContext } from "../../QueryASTContext";
import type { SelectionClause } from "../../selection/EntitySelection";
import { ReadOperation } from "../ReadOperation";
import type { OperationTranspileResult } from "../operations";
import { wrapSubqueriesInCypherCalls } from "../../../utils/wrap-subquery-in-calls";

export class CompositeReadPartial extends ReadOperation {
public transpile(context: QueryASTContext) {
Expand All @@ -51,16 +51,17 @@ export class CompositeReadPartial extends ReadOperation {
});

const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.filters, [nestedContext.target]);
const filterPredicates = this.getPredicates(nestedContext);
const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) =>
new Cypher.Call(sq).importWith(nestedContext.target)
);
const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext);

if (extraMatches.length > 0 || filterSubqueries.length > 0) {
if (extraMatches.length > 0 || filterSubqueries.length > 0 || authFilterSubqueries.length > 0) {
extraMatches = [matchClause, ...extraMatches];
matchClause = new Cypher.With("*");
}

const filterPredicates = this.getPredicates(nestedContext);
const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext);
const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext);

const wherePredicate = Cypher.and(filterPredicates, ...authFiltersPredicate);
if (wherePredicate) {
// NOTE: This is slightly different to ReadOperation for cypher compatibility, this could use `WITH *`
Expand All @@ -78,8 +79,8 @@ export class CompositeReadPartial extends ReadOperation {
const clause = Cypher.concat(
...extraMatches,
...filterSubqueries,
matchClause,
...authFilterSubqueries,
matchClause,
subqueries,
...sortSubqueries,
ret
Expand Down
129 changes: 129 additions & 0 deletions packages/graphql/tests/integration/issues/5066.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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 { createBearerToken } from "../../utils/create-bearer-token";
import type { UniqueType } from "../../utils/graphql-types";
import { TestHelper } from "../../utils/tests-helper";

describe("https://github.com/neo4j/graphql/issues/5066", () => {
let User: UniqueType;
let AdminGroup: UniqueType;
let UserBlockedUser: UniqueType;
let Party: UniqueType;

const secret = "secret";
const testHelper = new TestHelper();

beforeEach(async () => {
User = testHelper.createUniqueType("User");
AdminGroup = testHelper.createUniqueType("AdminGroup");
UserBlockedUser = testHelper.createUniqueType("UserBlockedUser");
Party = testHelper.createUniqueType("Party");

const typeDefs = /* GraphQL */ `
type ${AdminGroup} @node(labels: ["${AdminGroup}"]) @mutation(operations: []) @authorization(
filter: [
{ where: { node: { createdBy: { id: "$jwt.sub" } } } },
]
) {
id: ID! @id @unique
createdAt: DateTime! @timestamp(operations: [CREATE]) @private
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) @private
createdBy: ${User}! @relationship(type: "CREATED_ADMIN_GROUP", direction: IN) @settable(onCreate: true, onUpdate: false)
}
type ${User} @node(labels: ["${User}"]) @mutation(operations: []) @authorization(
filter: [
{ where: { node: { NOT: { blockedUsers_SOME: { to: { id: "$jwt.sub" } } } } } },
]
) {
id: ID! @unique @settable(onCreate: true, onUpdate: false)
createdAt: DateTime! @private
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) @private
username: String! @unique
blockedUsers: [${UserBlockedUser}!]! @relationship(type: "HAS_BLOCKED", direction: OUT)
createdAdminGroups: [${AdminGroup}!]! @relationship(type: "CREATED_ADMIN_GROUP", direction: OUT)
}
type ${UserBlockedUser} @node(labels: ["${UserBlockedUser}"]) @query(read: false, aggregate: false) @mutation(operations: []) @authorization(
filter: [
{ where: { node: { from: { id: "$jwt.sub" } } } }
]
) {
id: ID! @id @unique
createdAt: DateTime! @timestamp(operations: [CREATE]) @private
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) @private
from: ${User}! @relationship(type: "HAS_BLOCKED", direction: IN) @settable(onCreate: true, onUpdate: false)
to: ${User}! @relationship(type: "IS_BLOCKING", direction: OUT) @settable(onCreate: true, onUpdate: false)
}
union PartyCreator = ${User} | ${AdminGroup}
type ${Party} @node(labels: ["${Party}"]) @mutation(operations: []) @authorization(
filter: [
{ where: { node: { createdByConnection: { ${User}: { node: { id: "$jwt.sub" } } } } } },
{ where: { node: { createdByConnection: { ${AdminGroup}: { node: { createdBy: { id: "$jwt.sub" } } } } } } },
]
){
id: ID! @id @unique
createdAt: DateTime! @timestamp(operations: [CREATE]) @private
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) @private
createdBy: PartyCreator! @relationship(type: "CREATED_PARTY", direction: IN) @settable(onCreate: true, onUpdate: false)
}
`;
await testHelper.initNeo4jGraphQL({
typeDefs,
features: {
authorization: {
key: secret,
},
},
});
});

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

test("should return filtered results according to authorization rule", async () => {
const query = `
query Parties {
${Party.plural} {
id
createdBy {
... on ${User} {
username
}
}
}
}
`;

await testHelper.executeCypher(`
CREATE (p:${Party} { id: "1" })<-[:CREATED_PARTY]-(u:${User} { id: "1", username: "arthur" });
`);

const token = createBearerToken(secret, { sub: "1" });
const result = await testHelper.executeGraphQLWithToken(query, token);
expect(result.errors).toBeUndefined();
expect(result.data as any).toEqual({
[Party.plural]: [{ id: "1", createdBy: { username: "arthur" } }],
});
});
});
183 changes: 183 additions & 0 deletions packages/graphql/tests/tck/issues/5066.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* 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 { Neo4jGraphQL } from "../../../src";
import { createBearerToken } from "../../utils/create-bearer-token";
import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils";

describe("https://github.com/neo4j/graphql/issues/5066", () => {
let typeDefs: string;
let neoSchema: Neo4jGraphQL;
const secret = "secret";

beforeAll(() => {
typeDefs = /* GraphQL */ `
type AdminGroup
@node(labels: ["AdminGroup"])
@mutation(operations: [])
@authorization(filter: [{ where: { node: { createdBy: { id: "$jwt.sub" } } } }]) {
id: ID! @id @unique
createdAt: DateTime! @timestamp(operations: [CREATE]) @private
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) @private
createdBy: User!
@relationship(type: "CREATED_ADMIN_GROUP", direction: IN)
@settable(onCreate: true, onUpdate: false)
}
type User
@node(labels: ["User"])
@mutation(operations: [])
@authorization(
filter: [{ where: { node: { NOT: { blockedUsers_SOME: { to: { id: "$jwt.sub" } } } } } }]
) {
id: ID! @unique @settable(onCreate: true, onUpdate: false)
createdAt: DateTime! @private
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) @private
username: String! @unique
blockedUsers: [UserBlockedUser!]! @relationship(type: "HAS_BLOCKED", direction: OUT)
createdAdminGroups: [AdminGroup!]! @relationship(type: "CREATED_ADMIN_GROUP", direction: OUT)
}
type UserBlockedUser
@node(labels: ["UserBlockedUser"])
@query(read: false, aggregate: false)
@mutation(operations: [])
@authorization(filter: [{ where: { node: { from: { id: "$jwt.sub" } } } }]) {
id: ID! @id @unique
createdAt: DateTime! @timestamp(operations: [CREATE]) @private
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) @private
from: User! @relationship(type: "HAS_BLOCKED", direction: IN) @settable(onCreate: true, onUpdate: false)
to: User! @relationship(type: "IS_BLOCKING", direction: OUT) @settable(onCreate: true, onUpdate: false)
}
union PartyCreator = User | AdminGroup
type Party
@node(labels: ["Party"])
@mutation(operations: [])
@authorization(
filter: [
{ where: { node: { createdByConnection: { User: { node: { id: "$jwt.sub" } } } } } }
{
where: {
node: {
createdByConnection: { AdminGroup: { node: { createdBy: { id: "$jwt.sub" } } } }
}
}
}
]
) {
id: ID! @id @unique
createdAt: DateTime! @timestamp(operations: [CREATE]) @private
updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) @private
createdBy: PartyCreator!
@relationship(type: "CREATED_PARTY", direction: IN)
@settable(onCreate: true, onUpdate: false)
}
`;

neoSchema = new Neo4jGraphQL({
typeDefs,
features: {
authorization: {
key: secret,
},
},
});
});

test("filter unions with authotization", async () => {
const query = /* GraphQL */ `
query Parties {
parties {
id
createdBy {
... on User {
username
}
}
}
}
`;

const token = createBearerToken(secret, { sub: "1" });
const result = await translateQuery(neoSchema, query, {
contextValues: {
token,
},
});

expect(formatCypher(result.cypher)).toMatchInlineSnapshot(`
"MATCH (this:Party)
CALL {
WITH this
MATCH (this)<-[this0:CREATED_PARTY]-(this1:AdminGroup)
OPTIONAL MATCH (this1)<-[:CREATED_ADMIN_GROUP]-(this2:User)
WITH *, count(this2) AS createdByCount
WITH *
WHERE (createdByCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))
RETURN count(this1) > 0 AS var3
}
WITH *
WHERE (($isAuthenticated = true AND single(this4 IN [(this)<-[this5:CREATED_PARTY]-(this4:User) WHERE ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub) | 1] WHERE true)) OR ($isAuthenticated = true AND var3 = true))
CALL {
WITH this
CALL {
WITH *
MATCH (this)<-[this6:CREATED_PARTY]-(this7:User)
CALL {
WITH this7
MATCH (this7)-[:HAS_BLOCKED]->(this8:UserBlockedUser)
OPTIONAL MATCH (this8)-[:IS_BLOCKING]->(this9:User)
WITH *, count(this9) AS toCount
WITH *
WHERE (toCount <> 0 AND ($jwt.sub IS NOT NULL AND this9.id = $jwt.sub))
RETURN count(this8) > 0 AS var10
}
WITH *
WHERE ($isAuthenticated = true AND NOT (var10 = true))
WITH this7 { .username, __resolveType: \\"User\\", __id: id(this7) } AS this7
RETURN this7 AS var11
UNION
WITH *
MATCH (this)<-[this12:CREATED_PARTY]-(this13:AdminGroup)
OPTIONAL MATCH (this13)<-[:CREATED_ADMIN_GROUP]-(this14:User)
WITH *, count(this14) AS createdByCount
WITH *
WHERE ($isAuthenticated = true AND (createdByCount <> 0 AND ($jwt.sub IS NOT NULL AND this14.id = $jwt.sub)))
WITH this13 { __resolveType: \\"AdminGroup\\", __id: id(this13) } AS this13
RETURN this13 AS var11
}
WITH var11
RETURN head(collect(var11)) AS var11
}
RETURN this { .id, createdBy: var11 } AS this"
`);

expect(formatParams(result.params)).toMatchInlineSnapshot(`
"{
\\"jwt\\": {
\\"roles\\": [],
\\"sub\\": \\"1\\"
},
\\"isAuthenticated\\": true
}"
`);
});
});

0 comments on commit 4c09310

Please sign in to comment.