From 025705c697c51ff648bdc7a451ba4f9a74805196 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Thu, 26 Sep 2024 16:17:09 +0200 Subject: [PATCH 1/2] test: add test case for validate in authorization --- .../cypher-filtering-auth.int.test.ts | 124 ++++++++++++++++++ .../filtering/cypher-filtering-auth.test.ts | 71 ++++++++++ 2 files changed, 195 insertions(+) diff --git a/packages/graphql/tests/integration/directives/cypher/filtering/cypher-filtering-auth.int.test.ts b/packages/graphql/tests/integration/directives/cypher/filtering/cypher-filtering-auth.int.test.ts index 1cca31b21c..44e7f145c9 100644 --- a/packages/graphql/tests/integration/directives/cypher/filtering/cypher-filtering-auth.int.test.ts +++ b/packages/graphql/tests/integration/directives/cypher/filtering/cypher-filtering-auth.int.test.ts @@ -343,4 +343,128 @@ describe("cypher directive filtering", () => { ], }); }); + + test("With authorization on type using @cypher return value, with validate FAIL", async () => { + const Movie = testHelper.createUniqueType("Movie"); + const Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node @authorization(validate: [{ where: { node: { custom_field: "$jwt.custom_value" } } }]) { + title: String + custom_field: String + @cypher( + statement: """ + MATCH (this) + RETURN this.custom_field AS s + """ + columnName: "s" + ) + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + features: { + authorization: { + key: "secret", + }, + }, + }); + + const token = createBearerToken("secret", { custom_value: "hello" }); + + await testHelper.executeCypher( + ` + CREATE (m:${Movie} { title: "The Matrix" }) + CREATE (m2:${Movie} { title: "The Matrix 2", custom_field: "hello" }) + CREATE (m3:${Movie} { title: "The Matrix 3" }) + CREATE (a:${Actor} { name: "Keanu Reeves" }) + CREATE (a)-[:ACTED_IN]->(m) + `, + {} + ); + + const query = ` + query { + ${Movie.plural} { + title + } + } + `; + + const gqlResult = await testHelper.executeGraphQLWithToken(query, token); + + expect(gqlResult.errors).toHaveLength(1); + expect(gqlResult.errors?.[0]?.message).toBe("Forbidden"); + }); + + test("With authorization on type using @cypher return value, with validate PASS", async () => { + const Movie = testHelper.createUniqueType("Movie"); + const Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node @authorization(validate: [{ where: { node: { custom_field: "$jwt.custom_value" } } }]) { + title: String + custom_field: String + @cypher( + statement: """ + MATCH (this) + RETURN this.custom_field AS s + """ + columnName: "s" + ) + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + features: { + authorization: { + key: "secret", + }, + }, + }); + + const token = createBearerToken("secret", { custom_value: "hello" }); + + await testHelper.executeCypher( + ` + CREATE (m2:${Movie} { title: "The Matrix", custom_field: "hello" }) + CREATE (a:${Actor} { name: "Keanu Reeves" }) + CREATE (a)-[:ACTED_IN]->(m) + `, + {} + ); + + const query = ` + query { + ${Movie.plural} { + title + } + } + `; + + const gqlResult = await testHelper.executeGraphQLWithToken(query, token); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult?.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + }, + ], + }); + }); }); diff --git a/packages/graphql/tests/tck/directives/cypher/filtering/cypher-filtering-auth.test.ts b/packages/graphql/tests/tck/directives/cypher/filtering/cypher-filtering-auth.test.ts index 4599662fb3..810f03200c 100644 --- a/packages/graphql/tests/tck/directives/cypher/filtering/cypher-filtering-auth.test.ts +++ b/packages/graphql/tests/tck/directives/cypher/filtering/cypher-filtering-auth.test.ts @@ -335,4 +335,75 @@ describe("cypher directive filtering - Auth", () => { }" `); }); + + test("With authorization on type using @cypher return value, with validate", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node @authorization(validate: [{ where: { node: { custom_field: "$jwt.custom_value" } } }]) { + title: String + custom_field: String + @cypher( + statement: """ + MATCH (this) + RETURN this.custom_field AS s + """ + columnName: "s" + ) + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const token = createBearerToken("secret", { custom_value: "hello" }); + + const query = ` + query { + movies { + title + } + } + `; + + const neoSchema: Neo4jGraphQL = new Neo4jGraphQL({ + typeDefs, + features: { + authorization: { + key: "secret", + }, + }, + }); + + const result = await translateQuery(neoSchema, query, { token }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Movie) + CALL { + WITH this + CALL { + WITH this + WITH this AS this + MATCH (this) + RETURN this.custom_field AS s + } + WITH s AS this0 + RETURN this0 AS var1 + } + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.custom_value IS NOT NULL AND var1 = $jwt.custom_value)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .title } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": true, + \\"jwt\\": { + \\"roles\\": [], + \\"custom_value\\": \\"hello\\" + } + }" + `); + }); }); From 3b9acaa657b7fb23d00a45ca74b09d6ce43c15b3 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Fri, 27 Sep 2024 10:59:25 +0200 Subject: [PATCH 2/2] feat: add isNotNull check for cypher return variable in Auth cases --- .../queryAST/ast/filters/property-filters/CypherFilter.ts | 2 +- .../cypher/filtering/cypher-filtering-auth.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/CypherFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/CypherFilter.ts index a78df4e8af..fa80857f41 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/CypherFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/CypherFilter.ts @@ -86,7 +86,7 @@ export class CypherFilter extends Filter { }); if (this.checkIsNotNull) { - return Cypher.and(Cypher.isNotNull(this.comparisonValue), operation); + return Cypher.and(Cypher.isNotNull(this.comparisonValue), Cypher.isNotNull(this.returnVariable), operation); } return operation; diff --git a/packages/graphql/tests/tck/directives/cypher/filtering/cypher-filtering-auth.test.ts b/packages/graphql/tests/tck/directives/cypher/filtering/cypher-filtering-auth.test.ts index 810f03200c..96d949a993 100644 --- a/packages/graphql/tests/tck/directives/cypher/filtering/cypher-filtering-auth.test.ts +++ b/packages/graphql/tests/tck/directives/cypher/filtering/cypher-filtering-auth.test.ts @@ -76,7 +76,7 @@ describe("cypher directive filtering - Auth", () => { RETURN this0 AS var1 } WITH * - WHERE ($isAuthenticated = true AND ($jwt.custom_value IS NOT NULL AND var1 = $jwt.custom_value)) + WHERE ($isAuthenticated = true AND ($jwt.custom_value IS NOT NULL AND var1 IS NOT NULL AND var1 = $jwt.custom_value)) RETURN this { .title } AS this" `); @@ -248,7 +248,7 @@ describe("cypher directive filtering - Auth", () => { RETURN this1 AS var2 } WITH * - WHERE ($jwt.custom_value IS NOT NULL AND var2 = $jwt.custom_value) + WHERE ($jwt.custom_value IS NOT NULL AND var2 IS NOT NULL AND var2 = $jwt.custom_value) RETURN count(this0) > 0 AS var3 } WITH * @@ -321,7 +321,7 @@ describe("cypher directive filtering - Auth", () => { RETURN this0 AS var1 } WITH * - WHERE ($isAuthenticated = true AND ($jwt.custom_value IS NOT NULL AND var1 = $jwt.custom_value)) + WHERE ($isAuthenticated = true AND ($jwt.custom_value IS NOT NULL AND var1 IS NOT NULL AND var1 = $jwt.custom_value)) RETURN this { .title } AS this" `); @@ -392,7 +392,7 @@ describe("cypher directive filtering - Auth", () => { RETURN this0 AS var1 } WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.custom_value IS NOT NULL AND var1 = $jwt.custom_value)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.custom_value IS NOT NULL AND var1 IS NOT NULL AND var1 = $jwt.custom_value)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this { .title } AS this" `);