diff --git a/.changeset/quiet-bees-decide.md b/.changeset/quiet-bees-decide.md new file mode 100644 index 0000000000..1f7abc375e --- /dev/null +++ b/.changeset/quiet-bees-decide.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": patch +--- + +Fix: authorization checks are no longer added for the source nodes of connect operations, when the operation started with a create. The connect operation is likely required to complete before the authorization rules will be satisfied. diff --git a/packages/graphql/src/translate/create-connect-and-params.test.ts b/packages/graphql/src/translate/create-connect-and-params.test.ts index 1946d30b72..f82e090688 100644 --- a/packages/graphql/src/translate/create-connect-and-params.test.ts +++ b/packages/graphql/src/translate/create-connect-and-params.test.ts @@ -114,6 +114,7 @@ describe("createConnectAndParams", () => { refNodes: [node], parentNode: node, callbackBucket: new CallbackBucket(context), + source: "CONNECT", }); expect(result[0]).toMatchInlineSnapshot(` diff --git a/packages/graphql/src/translate/create-connect-and-params.ts b/packages/graphql/src/translate/create-connect-and-params.ts index cef0dee0f7..b7f1fa000d 100644 --- a/packages/graphql/src/translate/create-connect-and-params.ts +++ b/packages/graphql/src/translate/create-connect-and-params.ts @@ -50,6 +50,7 @@ function createConnectAndParams({ parentNode, includeRelationshipValidation, isFirstLevel = true, + source, }: { withVars: string[]; value: any; @@ -63,6 +64,7 @@ function createConnectAndParams({ parentNode: Node; includeRelationshipValidation?: boolean; isFirstLevel?: boolean; + source: "CREATE" | "UPDATE" | "CONNECT"; }): [string, any] { checkAuthentication({ context, node: parentNode, targetOperations: ["CREATE_RELATIONSHIP"] }); @@ -168,12 +170,16 @@ function createConnectAndParams({ } } + const authorizationNodes = [{ node: relatedNode, variable: nodeName }]; + // If the source is a create operation, it is likely that authorization + // rules are not satisfied until connect operation is complete + if (source !== "CREATE") { + authorizationNodes.push({ node: parentNode, variable: parentVar }); + } + const authorizationBeforeAndParams = createAuthorizationBeforeAndParams({ context, - nodes: [ - { node: parentNode, variable: parentVar }, - { node: relatedNode, variable: nodeName }, - ], + nodes: authorizationNodes, operations: ["CREATE_RELATIONSHIP"], }); @@ -356,6 +362,7 @@ function createConnectAndParams({ labelOverride: relField.union ? newRefNode.name : "", includeRelationshipValidation: true, isFirstLevel: false, + source: "CONNECT", }); r.connects.push(recurse[0]); r.params = { ...r.params, ...recurse[1] }; @@ -405,6 +412,7 @@ function createConnectAndParams({ parentNode: relatedNode, labelOverride: relField.union ? newRefNode.name : "", isFirstLevel: false, + source: "CONNECT", }); r.connects.push(recurse[0]); r.params = { ...r.params, ...recurse[1] }; diff --git a/packages/graphql/src/translate/create-create-and-params.ts b/packages/graphql/src/translate/create-create-and-params.ts index 5b0220d04a..22663b1485 100644 --- a/packages/graphql/src/translate/create-create-and-params.ts +++ b/packages/graphql/src/translate/create-create-and-params.ts @@ -215,6 +215,7 @@ function createCreateAndParams({ refNodes: [refNode], labelOverride: unionTypeName, parentNode: node, + source: "CREATE", }); res.creates.push(connectAndParams[0]); res.params = { ...res.params, ...connectAndParams[1] }; @@ -249,6 +250,7 @@ function createCreateAndParams({ refNodes, labelOverride: "", parentNode: node, + source: "CREATE", }); res.creates.push(connectAndParams[0]); res.params = { ...res.params, ...connectAndParams[1] }; diff --git a/packages/graphql/src/translate/create-update-and-params.ts b/packages/graphql/src/translate/create-update-and-params.ts index a3ad2d5f70..05e28617d6 100644 --- a/packages/graphql/src/translate/create-update-and-params.ts +++ b/packages/graphql/src/translate/create-update-and-params.ts @@ -372,6 +372,7 @@ export default function createUpdateAndParams({ relationField, labelOverride: relationField.union ? refNode.name : "", parentNode: node, + source: "UPDATE", }); subquery.push(connectAndParams[0]); if (context.subscriptionsEnabled) { diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index ba5c956192..e6196df5dd 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -239,6 +239,7 @@ export default async function translateUpdate({ parentNode: node, labelOverride: "", includeRelationshipValidation: !!assumeReconnecting, + source: "UPDATE", }); connectStrs.push(connectAndParams[0]); cypherParams = { ...cypherParams, ...connectAndParams[1] }; @@ -255,6 +256,7 @@ export default async function translateUpdate({ withVars, parentNode: node, labelOverride: relationField.union ? refNode.name : "", + source: "UPDATE", }); connectStrs.push(connectAndParams[0]); cypherParams = { ...cypherParams, ...connectAndParams[1] }; diff --git a/packages/graphql/tests/integration/issues/3888.int.test.ts b/packages/graphql/tests/integration/issues/3888.int.test.ts new file mode 100644 index 0000000000..e8bd392633 --- /dev/null +++ b/packages/graphql/tests/integration/issues/3888.int.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { Driver, Session } from "neo4j-driver"; +import { graphql } from "graphql"; +import Neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src"; +import { UniqueType } from "../../utils/graphql-types"; +import { cleanNodes } from "../../utils/clean-nodes"; +import { createBearerToken } from "../../utils/create-bearer-token"; + +describe("https://github.com/neo4j/graphql/issues/3888", () => { + let driver: Driver; + let neo4j: Neo4j; + let neoSchema: Neo4jGraphQL; + let session: Session; + const secret = "secret"; + + let Post: UniqueType; + let User: UniqueType; + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + }); + + beforeEach(async () => { + session = await neo4j.getSession(); + + Post = new UniqueType("Post"); + User = new UniqueType("User"); + + const typeDefs = ` + type ${User} { + id: ID! + } + + type ${Post} @authorization(filter: [{ where: { node: { author: { id: "$jwt.sub" } } } }]) { + title: String! + content: String! + author: ${User}! @relationship(type: "AUTHORED", direction: IN) + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + authorization: { + key: secret, + }, + }, + }); + }); + + afterEach(async () => { + await cleanNodes(session, [Post, User]); + await session.close(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("should not raise cardinality error when connecting on create", async () => { + const createUser = ` + mutation { + ${User.operations.create}(input: [{ id: "michel" }]) { + ${User.plural} { + id + } + } + } + `; + + const createPost = ` + mutation { + ${Post.operations.create}( + input: [ + { title: "Test1", content: "Test1", author: { connect: { where: { node: { id: "michel" } } } } } + ] + ) { + ${Post.plural} { + title + author { + id + } + } + } + } + `; + + const token = createBearerToken(secret, { sub: "michel" }); + + const createUserResult = await graphql({ + schema: await neoSchema.getSchema(), + source: createUser, + contextValue: neo4j.getContextValues({ token }), + }); + + expect(createUserResult.errors).toBeFalsy(); + + const createPostResult = await graphql({ + schema: await neoSchema.getSchema(), + source: createPost, + contextValue: neo4j.getContextValues({ token }), + }); + + expect(createPostResult.errors).toBeFalsy(); + expect(createPostResult.data).toEqual({ + [Post.operations.create]: { + [Post.plural]: [ + { + title: "Test1", + author: { + id: "michel", + }, + }, + ], + }, + }); + }); +}); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/allow/allow.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/allow/allow.test.ts index f5b1fee8a6..cb0abd424d 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/allow/allow.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/allow/allow.test.ts @@ -835,7 +835,7 @@ describe("Cypher Auth Allow", () => { OPTIONAL MATCH (this_connect_posts0_node:Post) OPTIONAL MATCH (this_connect_posts0_node)<-[:HAS_POST]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) CALL { WITH * WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts index 7061177041..376dfe6770 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts @@ -818,7 +818,7 @@ describe("Cypher Auth Where with Roles", () => { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0_node:Post) - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) CALL { WITH * WITH collect(this0_posts_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -897,7 +897,7 @@ describe("Cypher Auth Where with Roles", () => { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0_node:Post) - WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_param0 AND (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_param0 AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) CALL { WITH * WITH collect(this0_posts_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -966,7 +966,7 @@ describe("Cypher Auth Where with Roles", () => { CALL { WITH this OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this_posts0_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this_posts0_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) CALL { WITH * WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes @@ -1031,7 +1031,7 @@ describe("Cypher Auth Where with Roles", () => { CALL { WITH this OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_param0 AND (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this_posts0_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_param0 AND (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this_posts0_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) CALL { WITH * WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes @@ -1097,7 +1097,7 @@ describe("Cypher Auth Where with Roles", () => { CALL { WITH this OPTIONAL MATCH (this_connect_posts0_node:Post) - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this_connect_posts0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this_connect_posts0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) CALL { WITH * WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes @@ -1161,7 +1161,7 @@ describe("Cypher Auth Where with Roles", () => { CALL { WITH this OPTIONAL MATCH (this_connect_posts0_node:Post) - WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 AND (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this_connect_posts0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 AND (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (single(authorization_this0 IN [(this_connect_posts0_node)<-[:HAS_POST]-(authorization_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub) | 1] WHERE true) AND $authorization_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND (($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND $authorization_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND $authorization_param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) CALL { WITH * WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/where/interface-relationships/implementation-where.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/where/interface-relationships/implementation-where.test.ts index c6be2db5e2..fe97fc8961 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/where/interface-relationships/implementation-where.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/where/interface-relationships/implementation-where.test.ts @@ -672,7 +672,6 @@ describe("Cypher Auth Where", () => { CALL { WITH this0 OPTIONAL MATCH (this0_content_connect0_node:Comment) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) CALL { WITH * WITH collect(this0_content_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -691,7 +690,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this0_content_connect1_node:Post) OPTIONAL MATCH (this0_content_connect1_node)<-[:HAS_CONTENT]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) CALL { WITH * WITH collect(this0_content_connect1_node) as connectedNodes, collect(this0) as parentNodes @@ -760,7 +759,7 @@ describe("Cypher Auth Where", () => { CALL { WITH this0 OPTIONAL MATCH (this0_content_connect0_node:Comment) - WHERE this0_content_connect0_node.id = $this0_content_connect0_node_param0 AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WHERE this0_content_connect0_node.id = $this0_content_connect0_node_param0 CALL { WITH * WITH collect(this0_content_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -779,7 +778,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this0_content_connect1_node:Post) OPTIONAL MATCH (this0_content_connect1_node)<-[:HAS_CONTENT]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE this0_content_connect1_node.id = $this0_content_connect1_node_param0 AND (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE this0_content_connect1_node.id = $this0_content_connect1_node_param0 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) CALL { WITH * WITH collect(this0_content_connect1_node) as connectedNodes, collect(this0) as parentNodes @@ -804,6 +803,7 @@ describe("Cypher Auth Where", () => { \\"this0_name\\": \\"Bob\\", \\"this0_password\\": \\"password\\", \\"this0_content_connect0_node_param0\\": \\"post-id\\", + \\"this0_content_connect1_node_param0\\": \\"post-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -811,7 +811,6 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"this0_content_connect1_node_param0\\": \\"post-id\\", \\"resolvedCallbacks\\": {} }" `); @@ -866,7 +865,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this_content0_connect0_node:Post) OPTIONAL MATCH (this_content0_connect0_node)<-[:HAS_CONTENT]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE (($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) CALL { WITH * WITH collect(this_content0_connect0_node) as connectedNodes, collect(this) as parentNodes @@ -948,7 +947,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this_content0_connect0_node:Post) OPTIONAL MATCH (this_content0_connect0_node)<-[:HAS_CONTENT]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE this_content0_connect0_node.id = $this_content0_connect0_node_param0 AND (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE this_content0_connect0_node.id = $this_content0_connect0_node_param0 AND (($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) CALL { WITH * WITH collect(this_content0_connect0_node) as connectedNodes, collect(this) as parentNodes @@ -1023,7 +1022,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this_connect_content1_node:Post) OPTIONAL MATCH (this_connect_content1_node)<-[:HAS_CONTENT]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE (($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) CALL { WITH * WITH collect(this_connect_content1_node) as connectedNodes, collect(this) as parentNodes @@ -1096,7 +1095,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this_connect_content1_node:Post) OPTIONAL MATCH (this_connect_content1_node)<-[:HAS_CONTENT]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE this_connect_content1_node.id = $this_connect_content1_node_param0 AND (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE this_connect_content1_node.id = $this_connect_content1_node_param0 AND (($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) CALL { WITH * WITH collect(this_connect_content1_node) as connectedNodes, collect(this) as parentNodes diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts index 7bccb8b22c..35b4cade9a 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts @@ -815,7 +815,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this0_posts_connect0_node:Post) OPTIONAL MATCH (this0_posts_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) CALL { WITH * WITH collect(this0_posts_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -888,7 +888,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this0_posts_connect0_node:Post) OPTIONAL MATCH (this0_posts_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_param0 AND (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_param0 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) CALL { WITH * WITH collect(this0_posts_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -951,7 +951,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this_posts0_connect0_node:Post) OPTIONAL MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE (($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) CALL { WITH * WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes @@ -1008,7 +1008,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this_posts0_connect0_node:Post) OPTIONAL MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_param0 AND (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_param0 AND (($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) CALL { WITH * WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes @@ -1066,7 +1066,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this_connect_posts0_node:Post) OPTIONAL MATCH (this_connect_posts0_node)<-[:HAS_POST]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE (($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) CALL { WITH * WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes @@ -1124,7 +1124,7 @@ describe("Cypher Auth Where", () => { OPTIONAL MATCH (this_connect_posts0_node:Post) OPTIONAL MATCH (this_connect_posts0_node)<-[:HAS_POST]-(authorization_this0:User) WITH *, count(authorization_this0) AS creatorCount - WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 AND (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub)))) + WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 AND (($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_this0.id = $jwt.sub))) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) CALL { WITH * WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes diff --git a/packages/graphql/tests/tck/issues/3888.test.ts b/packages/graphql/tests/tck/issues/3888.test.ts new file mode 100644 index 0000000000..272185b1a9 --- /dev/null +++ b/packages/graphql/tests/tck/issues/3888.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { gql } from "graphql-tag"; +import type { DocumentNode } from "graphql"; +import { Neo4jGraphQL } from "../../../src"; +import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; +import { createBearerToken } from "../../utils/create-bearer-token"; + +describe("https://github.com/neo4j/graphql/issues/3888", () => { + const secret = "secret"; + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = gql` + type User { + id: ID! + } + + type Post @authorization(filter: [{ where: { node: { author: { id: "$jwt.sub" } } } }]) { + title: String! + content: String! + author: User! @relationship(type: "AUTHORED", direction: IN) + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { + authorization: { + key: secret, + }, + }, + }); + }); + + test("should not add an authorization check for connects coming from create", async () => { + const query = gql` + mutation { + createPosts( + input: [ + { title: "Test1", content: "Test1", author: { connect: { where: { node: { id: "michel" } } } } } + ] + ) { + posts { + title + } + } + } + `; + + const token = createBearerToken(secret, { sub: "michel" }); + const result = await translateQuery(neoSchema, query, { + token, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Post) + SET this0.title = $this0_title + SET this0.content = $this0_content + WITH this0 + CALL { + WITH this0 + OPTIONAL MATCH (this0_author_connect0_node:User) + WHERE this0_author_connect0_node.id = $this0_author_connect0_node_param0 + CALL { + WITH * + WITH collect(this0_author_connect0_node) as connectedNodes, collect(this0) as parentNodes + CALL { + WITH connectedNodes, parentNodes + UNWIND parentNodes as this0 + UNWIND connectedNodes as this0_author_connect0_node + MERGE (this0)<-[:AUTHORED]-(this0_author_connect0_node) + } + } + WITH this0, this0_author_connect0_node + RETURN count(*) AS connect_this0_author_connect_User + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)<-[this0_author_User_unique:AUTHORED]-(:User) + WITH count(this0_author_User_unique) as c + WHERE apoc.util.validatePredicate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDPost.author required exactly once', [0]) + RETURN c AS this0_author_User_unique_ignored + } + RETURN this0 + } + RETURN [this0 { .title }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"this0_title\\": \\"Test1\\", + \\"this0_content\\": \\"Test1\\", + \\"this0_author_connect0_node_param0\\": \\"michel\\", + \\"resolvedCallbacks\\": {} + }" + `); + }); +});