From d2f857f73540ee400f5dcc79cbb25dfba81c2963 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Fri, 23 Jul 2021 19:20:06 -0600 Subject: [PATCH] feat(graphql,#958,#1160): Enable authorizers on subscriptions * feat: authorizer applied to pub sub * feat(query-graphql): authorizer for websocket in resolvers * updating spaces to improve code review * feat: authorizer for create many * feat: authorizer applied to update subscriptions * feat: authorizer applied to delete subscriptions * fix: adding missing decorator to make authorizer applied to update and delete subscriptions working * feat: Enable authorizers on subscriptions * feat: Enable authorizers on subscriptions * example: Add authorized subscription example * fix: lock apollo versions until nestjs/graphql updates Co-authored-by: Armando Soriano --- examples/auth/src/app.module.ts | 14 +- examples/auth/src/sub-task/sub-task.module.ts | 1 + examples/auth/src/tag/tag.module.ts | 1 + .../auth/src/todo-item/todo-item.module.ts | 1 + examples/package.json | 2 + package-lock.json | 213 +++++++++++------- .../src/resolvers/create.resolver.ts | 33 ++- .../src/resolvers/delete.resolver.ts | 40 ++-- .../query-graphql/src/resolvers/helpers.ts | 6 +- .../src/resolvers/update.resolver.ts | 39 ++-- 10 files changed, 233 insertions(+), 117 deletions(-) diff --git a/examples/auth/src/app.module.ts b/examples/auth/src/app.module.ts index ad7fac552..4f2320b11 100644 --- a/examples/auth/src/app.module.ts +++ b/examples/auth/src/app.module.ts @@ -8,12 +8,24 @@ import { typeormOrmConfig } from '../../helpers'; import { AuthModule } from './auth/auth.module'; import { UserModule } from './user/user.module'; +interface HeadersContainer { + headers?: Record; +} +interface ContextArgs { + req?: HeadersContainer; + connection?: { context: HeadersContainer }; +} + @Module({ imports: [ TypeOrmModule.forRoot(typeormOrmConfig('auth')), GraphQLModule.forRoot({ autoSchemaFile: 'schema.gql', - context: ({ req }: { req: { headers: Record } }) => ({ req }), + installSubscriptionHandlers: true, + subscriptions: { + onConnect: (connectionParams: unknown) => ({ headers: connectionParams }), + }, + context: ({ req, connection }: ContextArgs) => ({ req: { ...req, ...connection?.context } }), }), AuthModule, UserModule, diff --git a/examples/auth/src/sub-task/sub-task.module.ts b/examples/auth/src/sub-task/sub-task.module.ts index ab1490dc6..fea713120 100644 --- a/examples/auth/src/sub-task/sub-task.module.ts +++ b/examples/auth/src/sub-task/sub-task.module.ts @@ -18,6 +18,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard'; CreateDTOClass: CreateSubTaskDTO, UpdateDTOClass: SubTaskUpdateDTO, enableAggregate: true, + enableSubscriptions: true, guards: [JwtAuthGuard], }, ], diff --git a/examples/auth/src/tag/tag.module.ts b/examples/auth/src/tag/tag.module.ts index 8c35976e5..f63985150 100644 --- a/examples/auth/src/tag/tag.module.ts +++ b/examples/auth/src/tag/tag.module.ts @@ -17,6 +17,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard'; CreateDTOClass: TagInputDTO, UpdateDTOClass: TagInputDTO, enableAggregate: true, + enableSubscriptions: true, guards: [JwtAuthGuard], }, ], diff --git a/examples/auth/src/todo-item/todo-item.module.ts b/examples/auth/src/todo-item/todo-item.module.ts index abec18d50..157373e9d 100644 --- a/examples/auth/src/todo-item/todo-item.module.ts +++ b/examples/auth/src/todo-item/todo-item.module.ts @@ -22,6 +22,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard'; CreateDTOClass: TodoItemInputDTO, UpdateDTOClass: TodoItemUpdateDTO, enableAggregate: true, + enableSubscriptions: true, guards: [JwtAuthGuard], }, ], diff --git a/examples/package.json b/examples/package.json index 854011efb..16c9db356 100644 --- a/examples/package.json +++ b/examples/package.json @@ -29,6 +29,8 @@ "@nestjs/sequelize": "8.0.0", "@nestjs/typeorm": "8.0.1", "@typegoose/typegoose": "8.0.0-beta.7", + "apollo-server-core": "2.25.2", + "apollo-server-types": "0.9.0", "apollo-server-express": "2.25.2", "apollo-server-plugin-base": "0.13.0", "class-validator": "0.13.1", diff --git a/package-lock.json b/package-lock.json index 25702366a..9a8c303d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -388,9 +388,9 @@ "integrity": "sha512-ZII+/xUFfb9ezDU2gad114+zScxVFMVlZ91f8fGApMzlS1kkqoyLnC4AJaQ1Ya/X+b63I20B4Gd+eCL8QuB4sA==" }, "@apollographql/graphql-playground-html": { - "version": "1.6.29", - "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.29.tgz", - "integrity": "sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==", + "version": "1.6.27", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.27.tgz", + "integrity": "sha512-tea2LweZvn6y6xFV11K0KC8ETjmm52mQrW+ezgB2O/aTQf8JGyFmMcRPFgUaQZeHbWdm8iisDC6EjOKsXu0nfw==", "requires": { "xss": "^1.0.8" } @@ -2031,30 +2031,6 @@ } } }, - "@graphql-tools/mock": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@graphql-tools/mock/-/mock-8.1.3.tgz", - "integrity": "sha512-xtY3amuEdPLeoSALNN4cEaOmietbVaxFAVfkn08v0AHr7zfXyy+sCLn98y8BXxTaow8/nTMBCTdCZ5Qe9gtbQQ==", - "requires": { - "@graphql-tools/schema": "^7.0.0", - "@graphql-tools/utils": "^7.0.0", - "fast-json-stable-stringify": "^2.1.0", - "ts-is-defined": "^1.0.0", - "tslib": "~2.2.0" - }, - "dependencies": { - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } - } - }, "@graphql-tools/module-loader": { "version": "6.2.7", "resolved": "https://registry.npmjs.org/@graphql-tools/module-loader/-/module-loader-6.2.7.tgz", @@ -7306,12 +7282,31 @@ } }, "apollo-datasource": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.0.2.tgz", - "integrity": "sha512-pZ7RDW2UVxxgSovshM27OHQ+rPlDjvGWdAQux6WswJrYEDx8sbJOr39GuyUBVL6yvNuIloOY6rsoG0NRqZ5mbQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.9.0.tgz", + "integrity": "sha512-y8H99NExU1Sk4TvcaUxTdzfq2SZo6uSj5dyh75XSQvbpH6gdAXIW9MaBcvlNC7n0cVPsidHmOcHOWxJ/pTXGjA==", "requires": { - "apollo-server-caching": "^3.0.1", - "apollo-server-env": "^4.0.2" + "apollo-server-caching": "^0.7.0", + "apollo-server-env": "^3.1.0" + }, + "dependencies": { + "apollo-server-caching": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz", + "integrity": "sha512-MsVCuf/2FxuTFVhGLK13B+TZH9tBd2qkyoXKKILIiGcZ5CDUEBO14vIV63aNkMkS1xxvK2U4wBcuuNj/VH2Mkw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "apollo-server-env": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-3.1.0.tgz", + "integrity": "sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ==", + "requires": { + "node-fetch": "^2.6.1", + "util.promisify": "^1.0.0" + } + } } }, "apollo-graphql": { @@ -7359,45 +7354,95 @@ } }, "apollo-server-core": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-3.0.2.tgz", - "integrity": "sha512-CDphrxAZdx1yt4CzVMKq5NOsaAKFzVnPBWzzL5Vi/BUTenCjqaVcRHxAVbjFEQJJv+Hv+RpEru51sLRjbJtRUQ==", + "version": "2.25.2", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.25.2.tgz", + "integrity": "sha512-lrohEjde2TmmDTO7FlOs8x5QQbAS0Sd3/t0TaK2TWaodfzi92QAvIsq321Mol6p6oEqmjm8POIDHW1EuJd7XMA==", "requires": { - "@apollographql/apollo-tools": "^0.5.1", - "@apollographql/graphql-playground-html": "1.6.29", - "@graphql-tools/mock": "^8.1.2", - "@graphql-tools/schema": "^7.1.5", - "@graphql-tools/utils": "^7.9.0", + "@apollographql/apollo-tools": "^0.5.0", + "@apollographql/graphql-playground-html": "1.6.27", + "@apollographql/graphql-upload-8-fork": "^8.1.3", "@josephg/resolvable": "^1.0.0", - "apollo-datasource": "^3.0.2", + "@types/ws": "^7.0.0", + "apollo-cache-control": "^0.14.0", + "apollo-datasource": "^0.9.0", "apollo-graphql": "^0.9.0", - "apollo-reporting-protobuf": "^3.0.0", - "apollo-server-caching": "^3.0.1", - "apollo-server-env": "^4.0.2", - "apollo-server-errors": "^3.0.1", - "apollo-server-plugin-base": "^3.0.2", - "apollo-server-types": "^3.0.2", + "apollo-reporting-protobuf": "^0.8.0", + "apollo-server-caching": "^0.7.0", + "apollo-server-env": "^3.1.0", + "apollo-server-errors": "^2.5.0", + "apollo-server-plugin-base": "^0.13.0", + "apollo-server-types": "^0.9.0", + "apollo-tracing": "^0.15.0", "async-retry": "^1.2.1", - "fast-json-stable-stringify": "^2.1.0", + "fast-json-stable-stringify": "^2.0.0", + "graphql-extensions": "^0.15.0", "graphql-tag": "^2.11.0", - "loglevel": "^1.6.8", + "graphql-tools": "^4.0.8", + "loglevel": "^1.6.7", "lru-cache": "^6.0.0", "sha.js": "^2.4.11", + "subscriptions-transport-ws": "^0.9.19", "uuid": "^8.0.0" }, "dependencies": { - "apollo-server-plugin-base": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-3.0.2.tgz", - "integrity": "sha512-WJ5PmGVUl67b41Z88EZDJcOqXFitsMbfyGDAqPQxeU2+bAl7+o1Oa9Hvx+qTsJKVKa9V+yS/nXFzZYpodehXEQ==", + "apollo-reporting-protobuf": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz", + "integrity": "sha512-B3XmnkH6Y458iV6OsA7AhfwvTgeZnFq9nPVjbxmLKnvfkEl8hYADtz724uPa0WeBiD7DSFcnLtqg9yGmCkBohg==", "requires": { - "apollo-server-types": "^3.0.2" + "@apollo/protobufjs": "1.2.2" } }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "apollo-server-caching": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz", + "integrity": "sha512-MsVCuf/2FxuTFVhGLK13B+TZH9tBd2qkyoXKKILIiGcZ5CDUEBO14vIV63aNkMkS1xxvK2U4wBcuuNj/VH2Mkw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "apollo-server-env": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-3.1.0.tgz", + "integrity": "sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ==", + "requires": { + "node-fetch": "^2.6.1", + "util.promisify": "^1.0.0" + } + }, + "apollo-server-errors": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.5.0.tgz", + "integrity": "sha512-lO5oTjgiC3vlVg2RKr3RiXIIQ5pGXBFxYGGUkKDhTud3jMIhs+gel8L8zsEjKaKxkjHhCQAA/bcEfYiKkGQIvA==" + }, + "apollo-server-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.9.0.tgz", + "integrity": "sha512-qk9tg4Imwpk732JJHBkhW0jzfG0nFsLqK2DY6UhvJf7jLnRePYsPxWfPiNkxni27pLE2tiNlCwoDFSeWqpZyBg==", + "requires": { + "apollo-reporting-protobuf": "^0.8.0", + "apollo-server-caching": "^0.7.0", + "apollo-server-env": "^3.1.0" + } + }, + "graphql-tools": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.8.tgz", + "integrity": "sha512-MW+ioleBrwhRjalKjYaLQbr+920pHBgy9vM/n47sswtns8+96sRn5M/G+J1eu7IMeKWiN/9p6tmwCHU7552VJg==", + "requires": { + "apollo-link": "^1.2.14", + "apollo-utilities": "^1.0.1", + "deprecated-decorator": "^0.1.6", + "iterall": "^1.1.3", + "uuid": "^3.1.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } } } }, @@ -7603,13 +7648,40 @@ } }, "apollo-server-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-3.0.2.tgz", - "integrity": "sha512-eZRLL96w9Ty/mV2j4QdoApNGRplwSNNPsAP9Wdlsf0eQ5WNFqgOWa/SQznyBXInlSrjhmnUaVjGZCsS9VoPD0w==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.9.0.tgz", + "integrity": "sha512-qk9tg4Imwpk732JJHBkhW0jzfG0nFsLqK2DY6UhvJf7jLnRePYsPxWfPiNkxni27pLE2tiNlCwoDFSeWqpZyBg==", "requires": { - "apollo-reporting-protobuf": "^3.0.0", - "apollo-server-caching": "^3.0.1", - "apollo-server-env": "^4.0.2" + "apollo-reporting-protobuf": "^0.8.0", + "apollo-server-caching": "^0.7.0", + "apollo-server-env": "^3.1.0" + }, + "dependencies": { + "apollo-reporting-protobuf": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz", + "integrity": "sha512-B3XmnkH6Y458iV6OsA7AhfwvTgeZnFq9nPVjbxmLKnvfkEl8hYADtz724uPa0WeBiD7DSFcnLtqg9yGmCkBohg==", + "requires": { + "@apollo/protobufjs": "1.2.2" + } + }, + "apollo-server-caching": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz", + "integrity": "sha512-MsVCuf/2FxuTFVhGLK13B+TZH9tBd2qkyoXKKILIiGcZ5CDUEBO14vIV63aNkMkS1xxvK2U4wBcuuNj/VH2Mkw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "apollo-server-env": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-3.1.0.tgz", + "integrity": "sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ==", + "requires": { + "node-fetch": "^2.6.1", + "util.promisify": "^1.0.0" + } + } } }, "apollo-tracing": { @@ -23095,14 +23167,6 @@ } } }, - "ts-is-defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ts-is-defined/-/ts-is-defined-1.0.0.tgz", - "integrity": "sha512-HmzqN8xWETXnfpXyUqMf5nvcZszn9aTNjxVIJ6R2aNNg14oLo3PCi9IRhsv+vg2C7TI90M7PyjBOrg4f6/nupA==", - "requires": { - "ts-tiny-invariant": "0.0.3" - } - }, "ts-jest": { "version": "27.0.4", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.4.tgz", @@ -23279,11 +23343,6 @@ "yn": "3.1.1" } }, - "ts-tiny-invariant": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/ts-tiny-invariant/-/ts-tiny-invariant-0.0.3.tgz", - "integrity": "sha512-EiaBUsUta7PPzVKpvZurcSDgaSkymxwiUc2rhX6Wu30bws2maipT6ihbEY072dU9lz6/FoFWEc6psXdlo0xqtg==" - }, "tsconfig-extends": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tsconfig-extends/-/tsconfig-extends-1.0.1.tgz", diff --git a/packages/query-graphql/src/resolvers/create.resolver.ts b/packages/query-graphql/src/resolvers/create.resolver.ts index 6be0fb4e2..acd01f88d 100644 --- a/packages/query-graphql/src/resolvers/create.resolver.ts +++ b/packages/query-graphql/src/resolvers/create.resolver.ts @@ -18,7 +18,7 @@ import { SubscriptionArgsType, SubscriptionFilterInputType, } from '../types'; -import { createSubscriptionFilter } from './helpers'; +import { createSubscriptionFilter, getSubscriptionEventName } from './helpers'; import { BaseServiceResolver, ResolverClass, ServiceResolver, SubscriptionResolverOpts } from './resolver.interface'; import { OperationGroup } from '../auth'; @@ -43,11 +43,14 @@ export interface CreateResolverOpts> extends Subscript } export interface CreateResolver> extends ServiceResolver { - createOne(input: MutationArgsType>): Promise; + createOne(input: MutationArgsType>, authorizeFilter?: Filter): Promise; - createMany(input: MutationArgsType>): Promise; + createMany(input: MutationArgsType>, authorizeFilter?: Filter): Promise; - createdSubscription(input?: SubscriptionArgsType): AsyncIterator>; + createdSubscription( + input?: SubscriptionArgsType, + authorizeFilter?: Filter, + ): AsyncIterator>; } /** @internal */ @@ -146,7 +149,7 @@ export const Creatable = // Ignore `authorizeFilter` for now but give users the ability to throw an UnauthorizedException const created = await this.service.createOne(input.input.input); if (enableOneSubscriptions) { - await this.publishCreatedEvent(created); + await this.publishCreatedEvent(created, authorizeFilter); } return created; } @@ -175,27 +178,33 @@ export const Creatable = // Ignore `authorizeFilter` for now but give users the ability to throw an UnauthorizedException const created = await this.service.createMany(input.input.input); if (enableManySubscriptions) { - await Promise.all(created.map((c) => this.publishCreatedEvent(c))); + await Promise.all(created.map((c) => this.publishCreatedEvent(c, authorizeFilter))); } return created; } - async publishCreatedEvent(dto: DTO): Promise { + async publishCreatedEvent(dto: DTO, authorizeFilter?: Filter): Promise { if (this.pubSub) { - await this.pubSub.publish(createdEvent, { [createdEvent]: dto }); + const eventName = getSubscriptionEventName(createdEvent, authorizeFilter); + await this.pubSub.publish(eventName, { [createdEvent]: dto }); } } @ResolverSubscription(() => DTOClass, { name: createdEvent, filter: subscriptionFilter }, commonResolverOpts, { enableSubscriptions: enableOneSubscriptions || enableManySubscriptions, + interceptors: [AuthorizerInterceptor(DTOClass)], }) - // input required so graphql subscription filtering will work. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - createdSubscription(@Args() input?: SA): AsyncIterator> { + createdSubscription( + @Args() input?: SA, + @AuthorizerFilter({ operationGroup: OperationGroup.CREATE, many: false }) + authorizeFilter?: Filter, + ): AsyncIterator> { if (!this.pubSub || !(enableManySubscriptions || enableOneSubscriptions)) { throw new Error(`Unable to subscribe to ${createdEvent}`); } - return this.pubSub.asyncIterator>(createdEvent); + + const eventName = getSubscriptionEventName(createdEvent, authorizeFilter); + return this.pubSub.asyncIterator>(eventName); } } return CreateResolverBase; diff --git a/packages/query-graphql/src/resolvers/delete.resolver.ts b/packages/query-graphql/src/resolvers/delete.resolver.ts index 153ea342b..a1f0393cc 100644 --- a/packages/query-graphql/src/resolvers/delete.resolver.ts +++ b/packages/query-graphql/src/resolvers/delete.resolver.ts @@ -15,7 +15,7 @@ import { SubscriptionFilterInputType, } from '../types'; import { MutationHookArgs, ResolverMutation, ResolverSubscription, AuthorizerFilter } from '../decorators'; -import { createSubscriptionFilter } from './helpers'; +import { createSubscriptionFilter, getSubscriptionEventName } from './helpers'; import { AuthorizerInterceptor, HookInterceptor } from '../interceptors'; import { OperationGroup } from '../auth'; @@ -39,9 +39,12 @@ export interface DeleteResolver, ): Promise; - deletedOneSubscription(input?: SubscriptionArgsType): AsyncIterator>>; + deletedOneSubscription( + input?: SubscriptionArgsType, + authorizeFilter?: Filter, + ): AsyncIterator>>; - deletedManySubscription(): AsyncIterator>; + deletedManySubscription(authorizeFilter?: Filter): AsyncIterator>; } /** @internal */ @@ -123,7 +126,7 @@ export const Deletable = ): Promise> { const deletedResponse = await this.service.deleteOne(input.input.id, { filter: authorizeFilter ?? {} }); if (enableOneSubscriptions) { - await this.publishDeletedOneEvent(deletedResponse); + await this.publishDeletedOneEvent(deletedResponse, authorizeFilter); } return deletedResponse; } @@ -147,20 +150,22 @@ export const Deletable = mergeFilter(input.input.filter, authorizeFilter ?? {}), ); if (enableManySubscriptions) { - await this.publishDeletedManyEvent(deleteManyResponse); + await this.publishDeletedManyEvent(deleteManyResponse, authorizeFilter); } return deleteManyResponse; } - async publishDeletedOneEvent(dto: DeleteOneResponse): Promise { + async publishDeletedOneEvent(dto: DeleteOneResponse, authorizeFilter?: Filter): Promise { if (this.pubSub) { - await this.pubSub.publish(deletedOneEvent, { [deletedOneEvent]: dto }); + const eventName = getSubscriptionEventName(deletedOneEvent, authorizeFilter); + await this.pubSub.publish(eventName, { [deletedOneEvent]: dto }); } } - async publishDeletedManyEvent(dmr: DeleteManyResponse): Promise { + async publishDeletedManyEvent(dmr: DeleteManyResponse, authorizeFilter?: Filter): Promise { if (this.pubSub) { - await this.pubSub.publish(deletedManyEvent, { [deletedManyEvent]: dmr }); + const eventName = getSubscriptionEventName(deletedManyEvent, authorizeFilter); + await this.pubSub.publish(eventName, { [deletedManyEvent]: dmr }); } } @@ -174,21 +179,30 @@ export const Deletable = ) // input required so graphql subscription filtering will work. // eslint-disable-next-line @typescript-eslint/no-unused-vars - deletedOneSubscription(@Args() input?: DOSA): AsyncIterator> { + deletedOneSubscription( + @Args() input?: DOSA, + @AuthorizerFilter({ operationGroup: OperationGroup.DELETE, many: false }) + authorizeFilter?: Filter, + ): AsyncIterator> { if (!enableOneSubscriptions || !this.pubSub) { throw new Error(`Unable to subscribe to ${deletedOneEvent}`); } - return this.pubSub.asyncIterator(deletedOneEvent); + const eventName = getSubscriptionEventName(deletedOneEvent, authorizeFilter); + return this.pubSub.asyncIterator(eventName); } @ResolverSubscription(() => DMR, { name: deletedManyEvent }, commonResolverOpts, { enableSubscriptions: enableManySubscriptions, }) - deletedManySubscription(): AsyncIterator> { + deletedManySubscription( + @AuthorizerFilter({ operationGroup: OperationGroup.DELETE, many: true }) + authorizeFilter?: Filter, + ): AsyncIterator> { if (!enableManySubscriptions || !this.pubSub) { throw new Error(`Unable to subscribe to ${deletedManyEvent}`); } - return this.pubSub.asyncIterator(deletedManyEvent); + const eventName = getSubscriptionEventName(deletedManyEvent, authorizeFilter); + return this.pubSub.asyncIterator(eventName); } } return DeleteResolverBase; diff --git a/packages/query-graphql/src/resolvers/helpers.ts b/packages/query-graphql/src/resolvers/helpers.ts index d314f893d..9d10342a5 100644 --- a/packages/query-graphql/src/resolvers/helpers.ts +++ b/packages/query-graphql/src/resolvers/helpers.ts @@ -1,4 +1,4 @@ -import { applyFilter, Class } from '@nestjs-query/core'; +import { applyFilter, Class, Filter } from '@nestjs-query/core'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; import { BadRequestException } from '@nestjs/common'; @@ -34,3 +34,7 @@ export const createSubscriptionFilter = } return true; }; + +export function getSubscriptionEventName(eventName: string, authorizeFilter?: Filter): string { + return authorizeFilter ? `${eventName}-${JSON.stringify(authorizeFilter)}` : eventName; +} diff --git a/packages/query-graphql/src/resolvers/update.resolver.ts b/packages/query-graphql/src/resolvers/update.resolver.ts index a0a9a9a80..aa0c074e2 100644 --- a/packages/query-graphql/src/resolvers/update.resolver.ts +++ b/packages/query-graphql/src/resolvers/update.resolver.ts @@ -23,7 +23,7 @@ import { } from '../types'; import { BaseServiceResolver, ResolverClass, ServiceResolver, SubscriptionResolverOpts } from './resolver.interface'; import { AuthorizerFilter, MutationHookArgs, ResolverMutation, ResolverSubscription } from '../decorators'; -import { createSubscriptionFilter } from './helpers'; +import { createSubscriptionFilter, getSubscriptionEventName } from './helpers'; import { AuthorizerInterceptor, HookInterceptor } from '../interceptors'; import { OperationGroup } from '../auth'; @@ -42,9 +42,9 @@ export interface UpdateResolver authFilter?: Filter, ): Promise; - updatedOneSubscription(input?: SubscriptionArgsType): AsyncIterator>; + updatedOneSubscription(input?: SubscriptionArgsType, authFilter?: Filter): AsyncIterator>; - updatedManySubscription(): AsyncIterator>; + updatedManySubscription(authFilter?: Filter): AsyncIterator>; } /** @internal */ @@ -157,7 +157,7 @@ export const Updateable = const { id, update } = input.input; const updateResult = await this.service.updateOne(id, update, { filter: authorizeFilter ?? {} }); if (enableOneSubscriptions) { - await this.publishUpdatedOneEvent(updateResult); + await this.publishUpdatedOneEvent(updateResult, authorizeFilter); } return updateResult; } @@ -185,20 +185,22 @@ export const Updateable = const { update, filter } = input.input; const updateManyResponse = await this.service.updateMany(update, mergeFilter(filter, authorizeFilter ?? {})); if (enableManySubscriptions) { - await this.publishUpdatedManyEvent(updateManyResponse); + await this.publishUpdatedManyEvent(updateManyResponse, authorizeFilter); } return updateManyResponse; } - async publishUpdatedOneEvent(dto: DTO): Promise { + async publishUpdatedOneEvent(dto: DTO, authorizeFilter?: Filter): Promise { if (this.pubSub) { - await this.pubSub.publish(updateOneEvent, { [updateOneEvent]: dto }); + const eventName = getSubscriptionEventName(updateOneEvent, authorizeFilter); + await this.pubSub.publish(eventName, { [updateOneEvent]: dto }); } } - async publishUpdatedManyEvent(umr: UpdateManyResponse): Promise { + async publishUpdatedManyEvent(umr: UpdateManyResponse, authorizeFilter?: Filter): Promise { if (this.pubSub) { - await this.pubSub.publish(updateManyEvent, { [updateManyEvent]: umr }); + const eventName = getSubscriptionEventName(updateManyEvent, authorizeFilter); + await this.pubSub.publish(eventName, { [updateManyEvent]: umr }); } } @@ -208,25 +210,36 @@ export const Updateable = commonResolverOpts, { enableSubscriptions: enableOneSubscriptions, + interceptors: [AuthorizerInterceptor(DTOClass)], }, ) // input required so graphql subscription filtering will work. // eslint-disable-next-line @typescript-eslint/no-unused-vars - updatedOneSubscription(@Args() input?: UOSA): AsyncIterator> { + updatedOneSubscription( + @Args() input?: UOSA, + @AuthorizerFilter({ operationGroup: OperationGroup.UPDATE, many: false }) + authorizeFilter?: Filter, + ): AsyncIterator> { if (!enableOneSubscriptions || !this.pubSub) { throw new Error(`Unable to subscribe to ${updateOneEvent}`); } - return this.pubSub.asyncIterator(updateOneEvent); + const eventName = getSubscriptionEventName(updateOneEvent, authorizeFilter); + return this.pubSub.asyncIterator(eventName); } @ResolverSubscription(() => UMR, { name: updateManyEvent }, commonResolverOpts, { enableSubscriptions: enableManySubscriptions, + interceptors: [AuthorizerInterceptor(DTOClass)], }) - updatedManySubscription(): AsyncIterator> { + updatedManySubscription( + @AuthorizerFilter({ operationGroup: OperationGroup.UPDATE, many: true }) + authorizeFilter?: Filter, + ): AsyncIterator> { if (!enableManySubscriptions || !this.pubSub) { throw new Error(`Unable to subscribe to ${updateManyEvent}`); } - return this.pubSub.asyncIterator(updateManyEvent); + const eventName = getSubscriptionEventName(updateManyEvent, authorizeFilter); + return this.pubSub.asyncIterator(eventName); } }