From 8d072173e6dbd4efc9ea4c119be56b4844d0f59c Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Wed, 12 Jul 2023 11:04:39 +0100 Subject: [PATCH 1/7] Add initial Federation authorization --- .eslintrc.js | 1 + .../translate/translate-resolve-reference.ts | 20 +- .../tck/federation/authorization.test.ts | 205 ++++++++++++++++++ .../graphql/tests/tck/utils/tck-test-utils.ts | 5 +- 4 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 packages/graphql/tests/tck/federation/authorization.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 3539ccd3ae..906f5e65a6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -61,6 +61,7 @@ module.exports = { prefer: "type-imports", }, ], + "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], }, settings: { "import/resolver": { diff --git a/packages/graphql/src/translate/translate-resolve-reference.ts b/packages/graphql/src/translate/translate-resolve-reference.ts index b9982ba7e0..5e36c660e1 100644 --- a/packages/graphql/src/translate/translate-resolve-reference.ts +++ b/packages/graphql/src/translate/translate-resolve-reference.ts @@ -38,7 +38,6 @@ export function translateResolveReference({ const matchNode = new Cypher.NamedNode(varName, { labels: node.getLabels(context) }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { __typename, ...where } = reference; const { @@ -61,6 +60,16 @@ export function translateResolveReference({ cypherFieldAliasMap: {}, }); + let projAuth: Cypher.Clause | undefined; + + const predicates: Cypher.Predicate[] = []; + + predicates.push(...projection.predicates); + + if (predicates.length) { + projAuth = new Cypher.With("*").where(Cypher.and(...predicates)); + } + const projectionSubqueries = Cypher.concat(...projection.subqueries, ...projection.subqueriesBeforeSort); const projectionExpression = new Cypher.RawCypher((env) => { @@ -69,20 +78,17 @@ export function translateResolveReference({ const returnClause = new Cypher.Return([projectionExpression, varName]); - const projectionClause: Cypher.Clause = returnClause; // TODO avoid reassign - let connectionPreClauses: Cypher.Clause | undefined; - const preComputedWhereFields = preComputedWhereFieldSubqueries && !preComputedWhereFieldSubqueries.empty ? Cypher.concat(preComputedWhereFieldSubqueries, topLevelWhereClause) - : undefined; + : topLevelWhereClause; const readQuery = Cypher.concat( topLevelMatch, preComputedWhereFields, - connectionPreClauses, + projAuth, projectionSubqueries, - projectionClause + returnClause ); return readQuery.build(undefined, context.cypherParams ? { cypherParams: context.cypherParams } : {}); diff --git a/packages/graphql/tests/tck/federation/authorization.test.ts b/packages/graphql/tests/tck/federation/authorization.test.ts new file mode 100644 index 0000000000..14b6037aa3 --- /dev/null +++ b/packages/graphql/tests/tck/federation/authorization.test.ts @@ -0,0 +1,205 @@ +/* + * 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 { Neo4jGraphQL } from "../../../src"; +import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; +import { createBearerToken } from "../../utils/create-bearer-token"; + +describe("Federation and authorization", () => { + test("type level", async () => { + const typeDefs = gql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type User @authorization(filter: [{ where: { node: { id: "$jwt.sub" } } }]) @key(fields: "id") { + id: ID! + name: String! + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { authorization: { key: "secret" } }, + }); + + const query = gql` + query ($representations: [_Any!]!) { + _entities(representations: $representations) { + ... on User { + id + name + } + } + } + `; + + const variableValues = { representations: [{ __typename: "User", id: "user" }] }; + + const token = createBearerToken("secret", { sub: "user" }); + + const result = await translateQuery(neoSchema, query, { + contextValues: { token }, + variableValues, + subgraph: true, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:\`User\`) + WITH * + WHERE (this.id = $param0 AND ($isAuthenticated = true AND this.id = coalesce($jwt.sub, $jwtDefault))) + RETURN this { .id, .name } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"user\\", + \\"isAuthenticated\\": true, + \\"jwt\\": { + \\"roles\\": [], + \\"sub\\": \\"user\\" + }, + \\"jwtDefault\\": {} + }" + `); + }); + + test("field level", async () => { + const typeDefs = gql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type User @key(fields: "id") { + id: ID! + name: String! + password: String! @authorization(filter: [{ where: { node: { id: "$jwt.sub" } } }]) + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { authorization: { key: "secret" } }, + }); + + const query = gql` + query ($representations: [_Any!]!) { + _entities(representations: $representations) { + ... on User { + id + name + password + } + } + } + `; + + const variableValues = { representations: [{ __typename: "User", id: "user" }] }; + + const token = createBearerToken("secret", { sub: "user" }); + + const result = await translateQuery(neoSchema, query, { + contextValues: { token }, + variableValues, + subgraph: true, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:\`User\`) + WHERE this.id = $param0 + WITH * + WHERE ($isAuthenticated = true AND this.id = coalesce($jwt.sub, $jwtDefault)) + RETURN this { .id, .name, .password } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"user\\", + \\"isAuthenticated\\": true, + \\"jwt\\": { + \\"roles\\": [], + \\"sub\\": \\"user\\" + }, + \\"jwtDefault\\": {} + }" + `); + }); + + test("with filter requiring subquery", async () => { + const typeDefs = gql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type User @key(fields: "id") { + id: ID! + name: String! + } + + type Post + @authorization(filter: [{ where: { node: { authorsAggregate: { count_GT: 2 } } } }]) + @key(fields: "id") { + id: ID! + content: String! + authors: [User!]! @relationship(type: "AUTHORED", direction: IN) + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { authorization: { key: "secret" } }, + }); + + const query = gql` + query ($representations: [_Any!]!) { + _entities(representations: $representations) { + ... on Post { + id + content + } + } + } + `; + + const variableValues = { representations: [{ __typename: "Post", id: "1" }] }; + + const token = createBearerToken("secret", { sub: "user" }); + + const result = await translateQuery(neoSchema, query, { + contextValues: { token }, + variableValues, + subgraph: true, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:\`Post\`) + CALL { + WITH this + MATCH (this)<-[this0:\`AUTHORED\`]-(this1:\`User\`) + RETURN count(this1) > $param0 AS var2 + } + WITH * + WHERE (this.id = $param1 AND ($isAuthenticated = true AND var2 = true)) + RETURN this { .id, .content } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"2\\", + \\"param1\\": \\"1\\", + \\"isAuthenticated\\": true + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/utils/tck-test-utils.ts b/packages/graphql/tests/tck/utils/tck-test-utils.ts index 6410e84c29..a717f9c836 100644 --- a/packages/graphql/tests/tck/utils/tck-test-utils.ts +++ b/packages/graphql/tests/tck/utils/tck-test-utils.ts @@ -62,10 +62,11 @@ export async function translateQuery( variableValues?: Record; neo4jVersion?: string; contextValues?: Record; + subgraph?: boolean; } ): Promise<{ cypher: string; params: Record }> { const driverBuilder = new DriverBuilder(); - const neo4jDatabaseInfo = new Neo4jDatabaseInfo(options?.neo4jVersion ?? "4.4"); + const neo4jDatabaseInfo = new Neo4jDatabaseInfo(options?.neo4jVersion ?? "5"); let contextValue: Record = { driver: driverBuilder.instance(), neo4jDatabaseInfo }; if (options?.req) { @@ -81,7 +82,7 @@ export async function translateQuery( } const graphqlArgs: GraphQLArgs = { - schema: await neoSchema.getSchema(), + schema: await (options?.subgraph ? neoSchema.getSubgraphSchema() : neoSchema.getSchema()), source: getQuerySource(query), contextValue, }; From 84d16806fa8fc19bc0570d83002ca15c18475b87 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Fri, 14 Jul 2023 10:49:02 +0100 Subject: [PATCH 2/7] Add E2E tests --- .../e2e/federation/authorization.e2e.test.ts | 194 ++++++++++++++++++ .../e2e/federation/setup/gateway-server.ts | 17 +- .../e2e/federation/setup/subgraph-server.ts | 2 + .../tests/e2e/federation/setup/subgraph.ts | 5 + 4 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 packages/graphql/tests/e2e/federation/authorization.e2e.test.ts diff --git a/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts b/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts new file mode 100644 index 0000000000..71b9819021 --- /dev/null +++ b/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts @@ -0,0 +1,194 @@ +/* + * 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 supertest from "supertest"; +import { UniqueType } from "../../utils/graphql-types"; +import { GatewayServer } from "./setup/gateway-server"; +import { Neo4j } from "./setup/neo4j"; +import type { Server } from "./setup/server"; +import { TestSubgraph } from "./setup/subgraph"; +import { SubgraphServer } from "./setup/subgraph-server"; +import { createBearerToken } from "../../utils/create-bearer-token"; + +describe("Federation 2 Authorization", () => { + let usersServer: Server; + let reviewsServer: Server; + let gatewayServer: Server; + + let neo4j: Neo4j; + + let gatewayUrl: string; + + let User: UniqueType; + let Review: UniqueType; + + beforeAll(async () => { + User = new UniqueType("User"); + Review = new UniqueType("Review"); + + const users = ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + + type ${User} @key(fields: "id") @shareable { + id: ID! + name: String + password: String @authorization(validate: [{ where: { node: { id: "$jwt.sub" } } }]) + } + `; + + const reviews = ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + + type ${User} @key(fields: "id", resolvable: false) @shareable { + id: ID! + } + + type ${Review} { + score: Int! + description: String! + author: ${User}! @relationship(type: "AUTHORED", direction: IN) + } + `; + + neo4j = new Neo4j(); + await neo4j.init(); + + const usersSubgraph = new TestSubgraph({ typeDefs: users, driver: neo4j.driver }); + const reviewsSubgraph = new TestSubgraph({ typeDefs: reviews, driver: neo4j.driver }); + + const [productsSchema, reviewsSchema] = await Promise.all([ + usersSubgraph.getSchema(), + reviewsSubgraph.getSchema(), + ]); + + usersServer = new SubgraphServer(productsSchema, 4008); + reviewsServer = new SubgraphServer(reviewsSchema, 4009); + + const [productsUrl, reviewsUrl] = await Promise.all([usersServer.start(), reviewsServer.start()]); + + gatewayServer = new GatewayServer( + [ + { name: "products", url: productsUrl }, + { name: "reviews", url: reviewsUrl }, + ], + 4010 + ); + + gatewayUrl = await gatewayServer.start(); + + await neo4j.executeWrite( + `CREATE (:${User} { id: "1", name: "user", password: "password" })-[:AUTHORED]->(:${Review} { score: 5, description: "review" })` + ); + }); + + afterAll(async () => { + await gatewayServer.stop(); + await Promise.all([usersServer.stop(), reviewsServer.stop()]); + await neo4j.close(); + }); + + test("should resolve when protected field is not queried", async () => { + const request = supertest(gatewayUrl); + + const response = await request + .post("") + .set({ Authorization: createBearerToken("secret", { sub: "baduser" }) }) + .send({ + query: ` + { + ${Review.plural} { + description + score + author { + id + name + } + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + data: { + [Review.plural]: [{ description: "review", score: 5, author: { id: "1", name: "user" } }], + }, + }); + }); + + test("should throw when protected field is queried as unauthorized user", async () => { + const request = supertest(gatewayUrl); + + const response = await request + .post("") + .set({ Authorization: createBearerToken("secret", { sub: "baduser" }) }) + .send({ + query: ` + { + ${Review.plural} { + description + score + author { + id + name + password + } + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.data).toEqual({ + [Review.plural]: [{ description: "review", score: 5, author: { id: "1", name: null, password: null } }], + }); + expect(response.body.errors[0].message).toBe("Forbidden"); + }); + + test("should resolve when protected field is queried as authorized user", async () => { + const request = supertest(gatewayUrl); + + const response = await request + .post("") + .set({ authorization: createBearerToken("secret", { sub: "1" }) }) + .send({ + query: ` + { + ${Review.plural} { + description + score + author { + id + name + password + } + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.data).toEqual({ + [Review.plural]: [ + { description: "review", score: 5, author: { id: "1", name: "user", password: "password" } }, + ], + }); + expect(response.body.errors).toBeUndefined(); + }); +}); diff --git a/packages/graphql/tests/e2e/federation/setup/gateway-server.ts b/packages/graphql/tests/e2e/federation/setup/gateway-server.ts index 871171fbdd..1bb791f519 100644 --- a/packages/graphql/tests/e2e/federation/setup/gateway-server.ts +++ b/packages/graphql/tests/e2e/federation/setup/gateway-server.ts @@ -19,7 +19,7 @@ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; -import { ApolloGateway, IntrospectAndCompose } from "@apollo/gateway"; +import { ApolloGateway, IntrospectAndCompose, RemoteGraphQLDataSource } from "@apollo/gateway"; import type { Server } from "./server"; import { getPort } from "./port"; @@ -34,10 +34,19 @@ export class GatewayServer implements Server { url?: string; constructor(subgraphs: Subgraph[], port?: number) { + class AuthenticatedDataSource extends RemoteGraphQLDataSource { + willSendRequest({ request, context }) { + request.http.headers.set("authorization", context.token); + } + } + const gateway = new ApolloGateway({ supergraphSdl: new IntrospectAndCompose({ subgraphs, }), + buildService({ url }) { + return new AuthenticatedDataSource({ url }); + }, }); this.server = new ApolloServer({ @@ -48,7 +57,11 @@ export class GatewayServer implements Server { } public async start(): Promise { - const { url } = await startStandaloneServer(this.server, { listen: { port: this.port } }); + const { url } = await startStandaloneServer(this.server, { + // eslint-disable-next-line @typescript-eslint/require-await + context: async ({ req }) => ({ token: req.headers.authorization }), + listen: { port: this.port }, + }); this.url = url; return url; } diff --git a/packages/graphql/tests/e2e/federation/setup/subgraph-server.ts b/packages/graphql/tests/e2e/federation/setup/subgraph-server.ts index ff9bc14bc6..98085b2e2c 100644 --- a/packages/graphql/tests/e2e/federation/setup/subgraph-server.ts +++ b/packages/graphql/tests/e2e/federation/setup/subgraph-server.ts @@ -38,6 +38,8 @@ export class SubgraphServer implements Server { public async start(): Promise { const { url } = await startStandaloneServer(this.server, { + // eslint-disable-next-line @typescript-eslint/require-await + context: async ({ req }) => ({ token: req.headers.authorization }), listen: { port: this.port }, }); this.url = url; diff --git a/packages/graphql/tests/e2e/federation/setup/subgraph.ts b/packages/graphql/tests/e2e/federation/setup/subgraph.ts index 98de9fc207..9a5141a27f 100644 --- a/packages/graphql/tests/e2e/federation/setup/subgraph.ts +++ b/packages/graphql/tests/e2e/federation/setup/subgraph.ts @@ -30,6 +30,11 @@ export class TestSubgraph { typeDefs, resolvers, driver, + features: { + authorization: { + key: "secret", + }, + }, }); } From b0ec8d9a32e05e15e42c34d3f30bd2a95b182869 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Fri, 14 Jul 2023 14:05:03 +0100 Subject: [PATCH 3/7] Update Federation E2E tests to work without port --- ...eration-subgraph-compatibility.e2e.test.ts | 72 +++++++++---------- .../utils/client.ts | 22 ------ .../e2e/federation/authorization.e2e.test.ts | 15 ++-- .../federation/entities-basics.e2e.test.ts | 15 ++-- .../e2e/federation/quickstart.e2e.test.ts | 15 ++-- .../e2e/federation/setup/gateway-server.ts | 9 +-- .../tests/e2e/federation/setup/port.ts | 24 ------- .../tests/e2e/federation/setup/server.ts | 1 - .../e2e/federation/setup/subgraph-server.ts | 8 +-- 9 files changed, 59 insertions(+), 122 deletions(-) delete mode 100644 packages/graphql/tests/e2e/federation/setup/port.ts diff --git a/packages/graphql/tests/e2e/federation/apollo-federation-subgraph-compatibility/apollo-federation-subgraph-compatibility.e2e.test.ts b/packages/graphql/tests/e2e/federation/apollo-federation-subgraph-compatibility/apollo-federation-subgraph-compatibility.e2e.test.ts index 2a1d8120b4..f63ccdf878 100644 --- a/packages/graphql/tests/e2e/federation/apollo-federation-subgraph-compatibility/apollo-federation-subgraph-compatibility.e2e.test.ts +++ b/packages/graphql/tests/e2e/federation/apollo-federation-subgraph-compatibility/apollo-federation-subgraph-compatibility.e2e.test.ts @@ -25,7 +25,7 @@ import { SubgraphServer } from "../setup/subgraph-server"; import { Neo4j } from "../setup/neo4j"; import { schema as inventory } from "./subgraphs/inventory"; import { schema as users } from "./subgraphs/users"; -import { productsRequest, routerRequest } from "./utils/client"; +import { graphqlRequest } from "./utils/client"; import { stripIgnoredCharacters } from "graphql"; describe("Tests copied from https://github.com/apollographql/apollo-federation-subgraph-compatibility", () => { @@ -37,6 +37,9 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s let neo4j: Neo4j; + let productsUrl: string; + let gatewayUrl: string; + beforeAll(async () => { const products = gql` extend schema @@ -122,8 +125,8 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s neo4j = new Neo4j(); await neo4j.init(); - inventoryServer = new SubgraphServer(inventory, 4010); - usersServer = new SubgraphServer(users, 4012); + inventoryServer = new SubgraphServer(inventory); + usersServer = new SubgraphServer(users); const productsSubgraph = new TestSubgraph({ typeDefs: products, @@ -142,24 +145,18 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s const productsSchema = await productsSubgraph.getSchema(); - productsServer = new SubgraphServer(productsSchema, 4011); + productsServer = new SubgraphServer(productsSchema); - const [inventoryUrl, productsUrl, usersUrl] = await Promise.all([ - inventoryServer.start(), - productsServer.start(), - usersServer.start(), - ]); + productsUrl = await productsServer.start(); + const [inventoryUrl, usersUrl] = await Promise.all([inventoryServer.start(), usersServer.start()]); - gatewayServer = new GatewayServer( - [ - { name: "inventory", url: inventoryUrl }, - { name: "products", url: productsUrl }, - { name: "users", url: usersUrl }, - ], - 4013 - ); + gatewayServer = new GatewayServer([ + { name: "inventory", url: inventoryUrl }, + { name: "products", url: productsUrl }, + { name: "users", url: usersUrl }, + ]); - await gatewayServer.start(); + gatewayUrl = await gatewayServer.start(); await neo4j.executeWrite( ` @@ -203,7 +200,8 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("ftv1", async () => { - const resp = await productsRequest( + const resp = await graphqlRequest( + productsUrl, { query: `query { __typename }`, }, @@ -222,7 +220,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s describe("@inaccessible", () => { it("should return @inaccessible directives in _service sdl", async () => { - const response = await productsRequest({ + const response = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); @@ -233,7 +231,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); it("should be able to query @inaccessible fields via the products schema directly", async () => { - const resp = await productsRequest({ + const resp = await graphqlRequest(productsUrl, { query: ` query GetProduct($id: ID!) { product(id: $id) { @@ -261,7 +259,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s describe("@key single", () => { test("applies single field @key on User", async () => { - const serviceSDLQuery = await productsRequest({ + const serviceSDLQuery = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); @@ -273,7 +271,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("resolves single field @key on User", async () => { - const resp = await productsRequest({ + const resp = await graphqlRequest(productsUrl, { query: `#graphql query ($representations: [_Any!]!) { _entities(representations: $representations) { @@ -302,7 +300,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s describe("@key multiple", () => { test("applies multiple field @key on DeprecatedProduct", async () => { - const serviceSDLQuery = await productsRequest({ + const serviceSDLQuery = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); @@ -312,7 +310,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("resolves multiple field @key on DeprecatedProduct", async () => { - const resp = await productsRequest({ + const resp = await graphqlRequest(productsUrl, { query: `#graphql query ($representations: [_Any!]!) { _entities(representations: $representations) { @@ -348,7 +346,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s describe("@key composite", () => { test("applies composite object @key on ProductResearch", async () => { - const serviceSDLQuery = await productsRequest({ + const serviceSDLQuery = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); @@ -358,7 +356,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("resolves composite object @key on ProductResearch", async () => { - const resp = await productsRequest({ + const resp = await graphqlRequest(productsUrl, { query: `#graphql query ($representations: [_Any!]!) { _entities(representations: $representations) { @@ -396,7 +394,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s describe("repeatable @key", () => { test("applies repeatable @key directive on Product", async () => { - const serviceSDLQuery = await productsRequest({ + const serviceSDLQuery = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); @@ -415,7 +413,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("resolves multiple @key directives on Product", async () => { - const entitiesQuery = await productsRequest({ + const entitiesQuery = await graphqlRequest(productsUrl, { query: `#graphql query ($representations: [_Any!]!) { _entities(representations: $representations) { @@ -466,7 +464,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("@link", async () => { - const response = await productsRequest({ + const response = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); @@ -544,7 +542,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s describe("@override", () => { it("should return @override directives in _service sdl", async () => { - const response = await productsRequest({ + const response = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); @@ -553,7 +551,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); it("should return overridden user name", async () => { - const resp = await routerRequest({ + const resp = await graphqlRequest(gatewayUrl, { query: ` query GetProduct($id: ID!) { product(id: $id) { @@ -580,7 +578,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("@provides", async () => { - const resp = await productsRequest({ + const resp = await graphqlRequest(productsUrl, { query: `#graphql query ($id: ID!) { product(id: $id) { @@ -607,7 +605,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("@requires", async () => { - const resp = await routerRequest({ + const resp = await graphqlRequest(gatewayUrl, { query: `#graphql query ($id: ID!) { product(id: $id) { createdBy { averageProductsCreatedPerYear email } } @@ -630,7 +628,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s describe("@shareable", () => { it("should return @shareable directives in _service sdl", async () => { - const response = await productsRequest({ + const response = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); @@ -639,7 +637,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); it("should be able to resolve @shareable ProductDimension types", async () => { - const resp = await routerRequest({ + const resp = await graphqlRequest(gatewayUrl, { query: ` query GetProduct($id: ID!) { product(id: $id) { @@ -668,7 +666,7 @@ describe("Tests copied from https://github.com/apollographql/apollo-federation-s }); test("@tag", async () => { - const response = await productsRequest({ + const response = await graphqlRequest(productsUrl, { query: "query { _service { sdl } }", }); diff --git a/packages/graphql/tests/e2e/federation/apollo-federation-subgraph-compatibility/utils/client.ts b/packages/graphql/tests/e2e/federation/apollo-federation-subgraph-compatibility/utils/client.ts index b63ea5b5cb..c2c9f0e4b8 100644 --- a/packages/graphql/tests/e2e/federation/apollo-federation-subgraph-compatibility/utils/client.ts +++ b/packages/graphql/tests/e2e/federation/apollo-federation-subgraph-compatibility/utils/client.ts @@ -35,28 +35,6 @@ export async function graphqlRequest( return resp.text(); } -export function productsRequest( - req: { - query: string; - variables?: { [key: string]: any }; - operationName?: string; - }, - headers?: { [key: string]: any } -) { - return graphqlRequest(PRODUCTS_URL, req, headers); -} - -export function routerRequest( - req: { - query: string; - variables?: { [key: string]: any }; - operationName?: string; - }, - headers?: { [key: string]: any } -) { - return graphqlRequest(ROUTER_URL, req, headers); -} - export async function healthcheckAll(libraryName: string): Promise { const routerUp = await healthcheckRouter(); if (!routerUp) { diff --git a/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts b/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts index 71b9819021..30dc1b480e 100644 --- a/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts +++ b/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts @@ -77,18 +77,15 @@ describe("Federation 2 Authorization", () => { reviewsSubgraph.getSchema(), ]); - usersServer = new SubgraphServer(productsSchema, 4008); - reviewsServer = new SubgraphServer(reviewsSchema, 4009); + usersServer = new SubgraphServer(productsSchema); + reviewsServer = new SubgraphServer(reviewsSchema); const [productsUrl, reviewsUrl] = await Promise.all([usersServer.start(), reviewsServer.start()]); - gatewayServer = new GatewayServer( - [ - { name: "products", url: productsUrl }, - { name: "reviews", url: reviewsUrl }, - ], - 4010 - ); + gatewayServer = new GatewayServer([ + { name: "products", url: productsUrl }, + { name: "reviews", url: reviewsUrl }, + ]); gatewayUrl = await gatewayServer.start(); diff --git a/packages/graphql/tests/e2e/federation/entities-basics.e2e.test.ts b/packages/graphql/tests/e2e/federation/entities-basics.e2e.test.ts index b8efec9394..9eaf302057 100644 --- a/packages/graphql/tests/e2e/federation/entities-basics.e2e.test.ts +++ b/packages/graphql/tests/e2e/federation/entities-basics.e2e.test.ts @@ -76,18 +76,15 @@ describe("Federation 2 Entities Basics (https://www.apollographql.com/docs/feder reviewsSubgraph.getSchema(), ]); - productsServer = new SubgraphServer(productsSchema, 4003); - reviewsServer = new SubgraphServer(reviewsSchema, 4004); + productsServer = new SubgraphServer(productsSchema); + reviewsServer = new SubgraphServer(reviewsSchema); const [productsUrl, reviewsUrl] = await Promise.all([productsServer.start(), reviewsServer.start()]); - gatewayServer = new GatewayServer( - [ - { name: "products", url: productsUrl }, - { name: "reviews", url: reviewsUrl }, - ], - 4005 - ); + gatewayServer = new GatewayServer([ + { name: "products", url: productsUrl }, + { name: "reviews", url: reviewsUrl }, + ]); gatewayUrl = await gatewayServer.start(); diff --git a/packages/graphql/tests/e2e/federation/quickstart.e2e.test.ts b/packages/graphql/tests/e2e/federation/quickstart.e2e.test.ts index ff712b10d5..35b13a1157 100644 --- a/packages/graphql/tests/e2e/federation/quickstart.e2e.test.ts +++ b/packages/graphql/tests/e2e/federation/quickstart.e2e.test.ts @@ -88,18 +88,15 @@ describe("Federation 2 quickstart (https://www.apollographql.com/docs/federation reviewsSubgraph.getSchema(), ]); - locationsServer = new SubgraphServer(locationsSchema, 4006); - reviewsServer = new SubgraphServer(reviewsSchema, 4007); + locationsServer = new SubgraphServer(locationsSchema); + reviewsServer = new SubgraphServer(reviewsSchema); const [locationsUrl, reviewsUrl] = await Promise.all([locationsServer.start(), reviewsServer.start()]); - gatewayServer = new GatewayServer( - [ - { name: "locations", url: locationsUrl }, - { name: "reviews", url: reviewsUrl }, - ], - 4008 - ); + gatewayServer = new GatewayServer([ + { name: "locations", url: locationsUrl }, + { name: "reviews", url: reviewsUrl }, + ]); gatewayUrl = await gatewayServer.start(); diff --git a/packages/graphql/tests/e2e/federation/setup/gateway-server.ts b/packages/graphql/tests/e2e/federation/setup/gateway-server.ts index 1bb791f519..3899a6cf7d 100644 --- a/packages/graphql/tests/e2e/federation/setup/gateway-server.ts +++ b/packages/graphql/tests/e2e/federation/setup/gateway-server.ts @@ -21,7 +21,6 @@ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { ApolloGateway, IntrospectAndCompose, RemoteGraphQLDataSource } from "@apollo/gateway"; import type { Server } from "./server"; -import { getPort } from "./port"; type Subgraph = { name: string; @@ -29,11 +28,10 @@ type Subgraph = { }; export class GatewayServer implements Server { - port: number; server: ApolloServer; url?: string; - constructor(subgraphs: Subgraph[], port?: number) { + constructor(subgraphs: Subgraph[]) { class AuthenticatedDataSource extends RemoteGraphQLDataSource { willSendRequest({ request, context }) { request.http.headers.set("authorization", context.token); @@ -52,15 +50,14 @@ export class GatewayServer implements Server { this.server = new ApolloServer({ gateway, }); - - this.port = port || getPort(); } public async start(): Promise { const { url } = await startStandaloneServer(this.server, { // eslint-disable-next-line @typescript-eslint/require-await context: async ({ req }) => ({ token: req.headers.authorization }), - listen: { port: this.port }, + // assign a random unused port + listen: { port: 0 }, }); this.url = url; return url; diff --git a/packages/graphql/tests/e2e/federation/setup/port.ts b/packages/graphql/tests/e2e/federation/setup/port.ts deleted file mode 100644 index 73f07342de..0000000000 --- a/packages/graphql/tests/e2e/federation/setup/port.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ - -let port = 4000; - -export const getPort = () => { - return port++; -}; diff --git a/packages/graphql/tests/e2e/federation/setup/server.ts b/packages/graphql/tests/e2e/federation/setup/server.ts index a64c1d74d7..31f2edada2 100644 --- a/packages/graphql/tests/e2e/federation/setup/server.ts +++ b/packages/graphql/tests/e2e/federation/setup/server.ts @@ -20,7 +20,6 @@ import type { ApolloServer } from "@apollo/server"; export interface Server { - port: number; server: ApolloServer; start(): Promise; stop(): Promise; diff --git a/packages/graphql/tests/e2e/federation/setup/subgraph-server.ts b/packages/graphql/tests/e2e/federation/setup/subgraph-server.ts index 98085b2e2c..decf9db22f 100644 --- a/packages/graphql/tests/e2e/federation/setup/subgraph-server.ts +++ b/packages/graphql/tests/e2e/federation/setup/subgraph-server.ts @@ -20,27 +20,25 @@ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import type { GraphQLSchema } from "graphql"; -import { getPort } from "./port"; import type { Server } from "./server"; export class SubgraphServer implements Server { - port: number; server: ApolloServer; url?: string; - constructor(schema: GraphQLSchema, port?: number) { + constructor(schema: GraphQLSchema) { this.server = new ApolloServer({ schema, includeStacktraceInErrorResponses: true, }); - this.port = port || getPort(); } public async start(): Promise { const { url } = await startStandaloneServer(this.server, { // eslint-disable-next-line @typescript-eslint/require-await context: async ({ req }) => ({ token: req.headers.authorization }), - listen: { port: this.port }, + // assign a random unused port + listen: { port: 0 }, }); this.url = url; return url; From f829c1ea017de0fdbefcd0444c00d807da9a2c42 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Fri, 14 Jul 2023 14:19:25 +0100 Subject: [PATCH 4/7] Test type level authorization --- .../e2e/federation/authorization.e2e.test.ts | 393 ++++++++++++------ 1 file changed, 265 insertions(+), 128 deletions(-) diff --git a/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts b/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts index 30dc1b480e..850dc44d90 100644 --- a/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts +++ b/packages/graphql/tests/e2e/federation/authorization.e2e.test.ts @@ -27,165 +27,302 @@ import { SubgraphServer } from "./setup/subgraph-server"; import { createBearerToken } from "../../utils/create-bearer-token"; describe("Federation 2 Authorization", () => { - let usersServer: Server; - let reviewsServer: Server; - let gatewayServer: Server; + describe("type authorization", () => { + let usersServer: Server; + let reviewsServer: Server; + let gatewayServer: Server; - let neo4j: Neo4j; + let neo4j: Neo4j; - let gatewayUrl: string; + let gatewayUrl: string; - let User: UniqueType; - let Review: UniqueType; + let User: UniqueType; + let Review: UniqueType; - beforeAll(async () => { - User = new UniqueType("User"); - Review = new UniqueType("Review"); + beforeAll(async () => { + User = new UniqueType("User"); + Review = new UniqueType("Review"); - const users = ` - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + const users = ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + + type ${User} @authorization(validate: [{ where: { node: { id: "$jwt.sub" } } }]) @key(fields: "id") @shareable { + id: ID! + name: String + password: String + } + `; - type ${User} @key(fields: "id") @shareable { - id: ID! - name: String - password: String @authorization(validate: [{ where: { node: { id: "$jwt.sub" } } }]) - } - `; + const reviews = ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + + type ${User} @key(fields: "id", resolvable: false) @shareable { + id: ID! + } + + type ${Review} { + score: Int! + description: String! + author: ${User}! @relationship(type: "AUTHORED", direction: IN) + } + `; - const reviews = ` - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + neo4j = new Neo4j(); + await neo4j.init(); - type ${User} @key(fields: "id", resolvable: false) @shareable { - id: ID! - } + const usersSubgraph = new TestSubgraph({ typeDefs: users, driver: neo4j.driver }); + const reviewsSubgraph = new TestSubgraph({ typeDefs: reviews, driver: neo4j.driver }); - type ${Review} { - score: Int! - description: String! - author: ${User}! @relationship(type: "AUTHORED", direction: IN) - } - `; + const [productsSchema, reviewsSchema] = await Promise.all([ + usersSubgraph.getSchema(), + reviewsSubgraph.getSchema(), + ]); - neo4j = new Neo4j(); - await neo4j.init(); + usersServer = new SubgraphServer(productsSchema); + reviewsServer = new SubgraphServer(reviewsSchema); - const usersSubgraph = new TestSubgraph({ typeDefs: users, driver: neo4j.driver }); - const reviewsSubgraph = new TestSubgraph({ typeDefs: reviews, driver: neo4j.driver }); + const [productsUrl, reviewsUrl] = await Promise.all([usersServer.start(), reviewsServer.start()]); - const [productsSchema, reviewsSchema] = await Promise.all([ - usersSubgraph.getSchema(), - reviewsSubgraph.getSchema(), - ]); + gatewayServer = new GatewayServer([ + { name: "products", url: productsUrl }, + { name: "reviews", url: reviewsUrl }, + ]); - usersServer = new SubgraphServer(productsSchema); - reviewsServer = new SubgraphServer(reviewsSchema); + gatewayUrl = await gatewayServer.start(); - const [productsUrl, reviewsUrl] = await Promise.all([usersServer.start(), reviewsServer.start()]); + await neo4j.executeWrite( + `CREATE (:${User} { id: "1", name: "user", password: "password" })-[:AUTHORED]->(:${Review} { score: 5, description: "review" })` + ); + }); - gatewayServer = new GatewayServer([ - { name: "products", url: productsUrl }, - { name: "reviews", url: reviewsUrl }, - ]); + afterAll(async () => { + await gatewayServer.stop(); + await Promise.all([usersServer.stop(), reviewsServer.stop()]); + await neo4j.close(); + }); - gatewayUrl = await gatewayServer.start(); + test("should throw when protected type is queried as unauthorized user", async () => { + const request = supertest(gatewayUrl); - await neo4j.executeWrite( - `CREATE (:${User} { id: "1", name: "user", password: "password" })-[:AUTHORED]->(:${Review} { score: 5, description: "review" })` - ); - }); + const response = await request + .post("") + .set({ Authorization: createBearerToken("secret", { sub: "baduser" }) }) + .send({ + query: ` + { + ${Review.plural} { + description + score + author { + id + name + password + } + } + } + `, + }); - afterAll(async () => { - await gatewayServer.stop(); - await Promise.all([usersServer.stop(), reviewsServer.stop()]); - await neo4j.close(); - }); + expect(response.status).toBe(200); + expect(response.body.data).toEqual({ + [Review.plural]: [{ description: "review", score: 5, author: { id: "1", name: null, password: null } }], + }); + expect(response.body.errors[0].message).toBe("Forbidden"); + }); + + test("should resolve when protected type is queried as authorized user", async () => { + const request = supertest(gatewayUrl); - test("should resolve when protected field is not queried", async () => { - const request = supertest(gatewayUrl); - - const response = await request - .post("") - .set({ Authorization: createBearerToken("secret", { sub: "baduser" }) }) - .send({ - query: ` - { - ${Review.plural} { - description - score - author { - id - name + const response = await request + .post("") + .set({ authorization: createBearerToken("secret", { sub: "1" }) }) + .send({ + query: ` + { + ${Review.plural} { + description + score + author { + id + name + password + } + } } - } - } - `, - }); + `, + }); - expect(response.status).toBe(200); - expect(response.body).toEqual({ - data: { - [Review.plural]: [{ description: "review", score: 5, author: { id: "1", name: "user" } }], - }, + expect(response.status).toBe(200); + expect(response.body.data).toEqual({ + [Review.plural]: [ + { description: "review", score: 5, author: { id: "1", name: "user", password: "password" } }, + ], + }); + expect(response.body.errors).toBeUndefined(); }); }); - test("should throw when protected field is queried as unauthorized user", async () => { - const request = supertest(gatewayUrl); - - const response = await request - .post("") - .set({ Authorization: createBearerToken("secret", { sub: "baduser" }) }) - .send({ - query: ` - { - ${Review.plural} { - description - score - author { - id - name - password - } + describe("field authorization", () => { + let usersServer: Server; + let reviewsServer: Server; + let gatewayServer: Server; + + let neo4j: Neo4j; + + let gatewayUrl: string; + + let User: UniqueType; + let Review: UniqueType; + + beforeAll(async () => { + User = new UniqueType("User"); + Review = new UniqueType("Review"); + + const users = ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + + type ${User} @key(fields: "id") @shareable { + id: ID! + name: String + password: String @authorization(validate: [{ where: { node: { id: "$jwt.sub" } } }]) } - } - `, - }); + `; + + const reviews = ` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + + type ${User} @key(fields: "id", resolvable: false) @shareable { + id: ID! + } + + type ${Review} { + score: Int! + description: String! + author: ${User}! @relationship(type: "AUTHORED", direction: IN) + } + `; - expect(response.status).toBe(200); - expect(response.body.data).toEqual({ - [Review.plural]: [{ description: "review", score: 5, author: { id: "1", name: null, password: null } }], + neo4j = new Neo4j(); + await neo4j.init(); + + const usersSubgraph = new TestSubgraph({ typeDefs: users, driver: neo4j.driver }); + const reviewsSubgraph = new TestSubgraph({ typeDefs: reviews, driver: neo4j.driver }); + + const [productsSchema, reviewsSchema] = await Promise.all([ + usersSubgraph.getSchema(), + reviewsSubgraph.getSchema(), + ]); + + usersServer = new SubgraphServer(productsSchema); + reviewsServer = new SubgraphServer(reviewsSchema); + + const [productsUrl, reviewsUrl] = await Promise.all([usersServer.start(), reviewsServer.start()]); + + gatewayServer = new GatewayServer([ + { name: "products", url: productsUrl }, + { name: "reviews", url: reviewsUrl }, + ]); + + gatewayUrl = await gatewayServer.start(); + + await neo4j.executeWrite( + `CREATE (:${User} { id: "1", name: "user", password: "password" })-[:AUTHORED]->(:${Review} { score: 5, description: "review" })` + ); }); - expect(response.body.errors[0].message).toBe("Forbidden"); - }); - test("should resolve when protected field is queried as authorized user", async () => { - const request = supertest(gatewayUrl); - - const response = await request - .post("") - .set({ authorization: createBearerToken("secret", { sub: "1" }) }) - .send({ - query: ` - { - ${Review.plural} { - description - score - author { - id - name - password + afterAll(async () => { + await gatewayServer.stop(); + await Promise.all([usersServer.stop(), reviewsServer.stop()]); + await neo4j.close(); + }); + + test("should resolve when protected field is not queried", async () => { + const request = supertest(gatewayUrl); + + const response = await request + .post("") + .set({ Authorization: createBearerToken("secret", { sub: "baduser" }) }) + .send({ + query: ` + { + ${Review.plural} { + description + score + author { + id + name + } + } } - } - } - `, + `, + }); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + data: { + [Review.plural]: [{ description: "review", score: 5, author: { id: "1", name: "user" } }], + }, + }); + }); + + test("should throw when protected field is queried as unauthorized user", async () => { + const request = supertest(gatewayUrl); + + const response = await request + .post("") + .set({ Authorization: createBearerToken("secret", { sub: "baduser" }) }) + .send({ + query: ` + { + ${Review.plural} { + description + score + author { + id + name + password + } + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.data).toEqual({ + [Review.plural]: [{ description: "review", score: 5, author: { id: "1", name: null, password: null } }], }); + expect(response.body.errors[0].message).toBe("Forbidden"); + }); + + test("should resolve when protected field is queried as authorized user", async () => { + const request = supertest(gatewayUrl); - expect(response.status).toBe(200); - expect(response.body.data).toEqual({ - [Review.plural]: [ - { description: "review", score: 5, author: { id: "1", name: "user", password: "password" } }, - ], + const response = await request + .post("") + .set({ authorization: createBearerToken("secret", { sub: "1" }) }) + .send({ + query: ` + { + ${Review.plural} { + description + score + author { + id + name + password + } + } + } + `, + }); + + expect(response.status).toBe(200); + expect(response.body.data).toEqual({ + [Review.plural]: [ + { description: "review", score: 5, author: { id: "1", name: "user", password: "password" } }, + ], + }); + expect(response.body.errors).toBeUndefined(); }); - expect(response.body.errors).toBeUndefined(); }); }); From 1ace55f0a9cafca8793b086c4fd8a18045cda466 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Fri, 14 Jul 2023 14:20:16 +0100 Subject: [PATCH 5/7] Add changeset --- .changeset/spotty-snakes-fry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spotty-snakes-fry.md diff --git a/.changeset/spotty-snakes-fry.md b/.changeset/spotty-snakes-fry.md new file mode 100644 index 0000000000..cce77dcce3 --- /dev/null +++ b/.changeset/spotty-snakes-fry.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": minor +--- + +The evaluation of authorization rules is now supported when using the Neo4j GraphQL Library as a Federation Subgraph. From b25e4365d9f41afe7f7e9415e854eaefdc25eeef Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Fri, 14 Jul 2023 15:03:14 +0100 Subject: [PATCH 6/7] Update TCK tests --- .../graphql/tests/tck/federation/authorization.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/graphql/tests/tck/federation/authorization.test.ts b/packages/graphql/tests/tck/federation/authorization.test.ts index 14b6037aa3..be19a4298e 100644 --- a/packages/graphql/tests/tck/federation/authorization.test.ts +++ b/packages/graphql/tests/tck/federation/authorization.test.ts @@ -60,7 +60,7 @@ describe("Federation and authorization", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`User\`) + "MATCH (this:User) WITH * WHERE (this.id = $param0 AND ($isAuthenticated = true AND this.id = coalesce($jwt.sub, $jwtDefault))) RETURN this { .id, .name } AS this" @@ -118,7 +118,7 @@ describe("Federation and authorization", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`User\`) + "MATCH (this:User) WHERE this.id = $param0 WITH * WHERE ($isAuthenticated = true AND this.id = coalesce($jwt.sub, $jwtDefault)) @@ -183,10 +183,10 @@ describe("Federation and authorization", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Post\`) + "MATCH (this:Post) CALL { WITH this - MATCH (this)<-[this0:\`AUTHORED\`]-(this1:\`User\`) + MATCH (this)<-[this0:AUTHORED]-(this1:User) RETURN count(this1) > $param0 AS var2 } WITH * @@ -196,7 +196,7 @@ describe("Federation and authorization", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"param0\\": \\"2\\", + \\"param0\\": 2, \\"param1\\": \\"1\\", \\"isAuthenticated\\": true }" From 2a7814249c3edfe56b1bb6a995433484b7523992 Mon Sep 17 00:00:00 2001 From: Darrell Warde Date: Fri, 14 Jul 2023 15:06:03 +0100 Subject: [PATCH 7/7] Skip Federation tests for GraphQL 15 --- .../tests/tck/federation/authorization.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/graphql/tests/tck/federation/authorization.test.ts b/packages/graphql/tests/tck/federation/authorization.test.ts index be19a4298e..b971ae9223 100644 --- a/packages/graphql/tests/tck/federation/authorization.test.ts +++ b/packages/graphql/tests/tck/federation/authorization.test.ts @@ -21,9 +21,15 @@ import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../../src"; import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; import { createBearerToken } from "../../utils/create-bearer-token"; +import { versionInfo } from "graphql"; describe("Federation and authorization", () => { test("type level", async () => { + if (versionInfo.major < 16) { + console.log("GraphQL version is <16, skipping Federation tests"); + return; + } + const typeDefs = gql` extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) @@ -80,6 +86,11 @@ describe("Federation and authorization", () => { }); test("field level", async () => { + if (versionInfo.major < 16) { + console.log("GraphQL version is <16, skipping Federation tests"); + return; + } + const typeDefs = gql` extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) @@ -139,6 +150,11 @@ describe("Federation and authorization", () => { }); test("with filter requiring subquery", async () => { + if (versionInfo.major < 16) { + console.log("GraphQL version is <16, skipping Federation tests"); + return; + } + const typeDefs = gql` extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])