diff --git a/documentation/docs/graphql/authorization.mdx b/documentation/docs/graphql/authorization.mdx index 2ae0a2c56..8de2caaab 100644 --- a/documentation/docs/graphql/authorization.mdx +++ b/documentation/docs/graphql/authorization.mdx @@ -13,10 +13,10 @@ The following section assumes you are familiar with [authentication in nestjs](h The `nestjs-query` graphql package exposes decorators and options to allow the following -* Additional filtering for objects based on the graphql context. -* Filtering relations based on the graphql context. -* Low level authorization service support when your authorizer needs to use other services or additional information -that is not in the graphql context. +- Additional filtering for objects based on the graphql context. +- Filtering relations based on the graphql context. +- Low level authorization service support when your authorizer needs to use other services or additional information + that is not in the graphql context. :::info If you are looking to modify incoming requests based on the context, take a look at the [hooks documentation](./hooks.mdx) @@ -31,11 +31,10 @@ Authorization is invoked as the last step before calling the `QueryService`. All examples assume you have a guard that adds a `user` to the req on the context. ```ts - type AuthenticatedUser = { id: number; username: string; -} +}; type UserContext = { req: { @@ -85,19 +84,21 @@ The `@nestjs-query/query-graphql` package includes a `@Authorize` decorator that criteria to authorize an incoming request. The `@Authorize` decorator accepts the following types. -* An `object` that has an `authorize` method that returns a Filter for the DTO. -* An instance of an `Authorizer` that implements the `authorize` and `authorizeRelation` methods. -* An `Authorizer` class reference that implements the `Authorizer` interface. The `Authorizer` class will be -instantiated using the `nestjs`'s dependency injection. + +- An `object` that has an `authorize` method that returns a Filter for the DTO. +- An instance of an `Authorizer` that implements the `authorize` and `authorizeRelation` methods. +- An `Authorizer` class reference that implements the `Authorizer` interface. The `Authorizer` class will be + instantiated using the `nestjs`'s dependency injection. The `@Authorize` decorator does not return an unauthorized error instead the following will occur: - * `queryMany` results will not include any DTOs that do not match the filter criteria. - * `findOne` will return a not found for a DTO that is cannot be found for the `id` and auth filter. - * `updateOne` will return a not found error if the DTO to update cannot be found for the `id` and auth filter. - * `updateMany` will exclude any records that do not match the user provided filter and the auth filter from being - updated. - * `deleteOne` will return a not found error if the DTO to delete cannot be found for the `id` and auth filter. - * `deleteMany` will exclude any records that do not match the user provided filter and the auth filter from being + +- `queryMany` results will not include any DTOs that do not match the filter criteria. +- `findOne` will return a not found for a DTO that is cannot be found for the `id` and auth filter. +- `updateOne` will return a not found error if the DTO to update cannot be found for the `id` and auth filter. +- `updateMany` will exclude any records that do not match the user provided filter and the auth filter from being + updated. +- `deleteOne` will return a not found error if the DTO to delete cannot be found for the `id` and auth filter. +- `deleteMany` will exclude any records that do not match the user provided filter and the auth filter from being deleted. :::note @@ -155,7 +156,6 @@ export class TodoItemDTO { @FilterableField() ownerId!: number; } - ``` :::note @@ -169,11 +169,12 @@ By default when relations are queried any additional filters defined using the ` DTO will also be included. When mutating relations -* If the DTO that is having a relation(s) added or removed cannot be found for the `id` and -auth filter a not found error will be returned. -* When adding or removing a single relation if the relation cannot be found for the `id` and auth filter a not found -error will be returned. -* When adding or removing multiple relations if all relations cannot be found a not found error will be throw. + +- If the DTO that is having a relation(s) added or removed cannot be found for the `id` and + auth filter a not found error will be returned. +- When adding or removing a single relation if the relation cannot be found for the `id` and auth filter a not found + error will be returned. +- When adding or removing multiple relations if all relations cannot be found a not found error will be throw. For example given the following `SubTaskDTO` definition whenever the `subTasks` connection is queried through a `todoItem`, only `subTasks` that belong to the user will be returned. @@ -240,13 +241,13 @@ For example you could define the subtasks with the `auth` option, only allowing ### Custom Authorizer When you need more control over authorization you can create a custom `Authorizer`. You may want to use a custom - `Authorizer` if you need to use additional services to do authorization for a DTO. +`Authorizer` if you need to use additional services to do authorization for a DTO. The `Authorizer` interface requires two methods to be implemented -* `authorize` - Should return a filter that should be used for all queries and mutations for the DTO. -* `authorizeRelation` - Should return a filter for the relation that will be used when querying the relation or -adding/removing relations to/from the DTO. +- `authorize` - Should return a filter that should be used for all queries and mutations for the DTO. +- `authorizeRelation` - Should return a filter for the relation that will be used when querying the relation or + adding/removing relations to/from the DTO. In this example we'll create a simple authorizer for `SubTasks`. You can use this as a base to create a more complex authorizers that depends on other services. @@ -324,8 +325,9 @@ The easiest way to leverage `Authorizers` in a custom resolver is to use the `Au `AuthorizerFilter` param decorator. In this example there are two important additions: + 1. The `AuthorizerInterceptor` is added to the `TodoItemResolver` as an interceptor, this interceptor will add the - authorizer to the context so it can be used down stream + authorizer to the context so it can be used down stream 2. The `AuthorizerFilter` param decorator uses the authorizer added by the interceptor to create an authorizer filter. ```ts title="todo-item/todo-item.resolver.ts" {9,17} @@ -379,18 +381,73 @@ import { TodoItemEntity } from './todo-item.entity'; export class TodoItemResolver extends CRUDResolver(TodoItemDTO) { constructor( @InjectQueryService(TodoItemEntity) readonly service: QueryService, - @InjectAuthorizer(TodoItemDTO) readonly authorizer: Authorizer + @InjectAuthorizer(TodoItemDTO) readonly authorizer: Authorizer, ) { super(service); } } - ``` :::important If you are extending the `CRUDResolver` directly be sure to [register your DTOs with the `NestjsQueryGraphQLModule`](./resolvers.mdx#crudresolver) ::: +## Authorize depending on operation + +Sometimes it might be necessary to perform different authorization based on the kind of operation an user wants to execute. +E.g. some users could be allowed to read all todo items but only update/delete their own. + +In this case we can make use of the second parameter of the `authorize` function in our custom `Authorizer` or the one passed to the `@Authorizer` decorator which gets passed the name of the operation that should be authorized: + +```ts title='sub-task/sub-task.authorizer.ts' +import { Injectable } from '@nestjs/common'; +import { Authorizer } from '@nestjs-query/query-graphql'; +import { Filter } from '@nestjs-query/core'; +import { UserContext } from '../auth/auth.interfaces'; +import { SubTaskDTO } from './dto/sub-task.dto'; + +@Injectable() +export class SubTaskAuthorizer implements Authorizer { + authorize(context: UserContext, operationName?: string): Promise> { + if ( + operationName.startsWith('query') || + operationName.startsWith('find') || + operationName.startsWith('aggregate') + ) { + return Promise.resolve({}); + } + + return Promise.resolve({ ownerId: { eq: context.req.user.id } }); + } + + authorizeRelation(relationName: string, context: UserContext): Promise> { + if (relationName === 'todoItem') { + return Promise.resolve({ ownerId: { eq: context.req.user.id } }); + } + return Promise.resolve({}); + } +} +``` + +The operation name is the name of the method that makes use of the `@AuthorizerFilter()` or the argument passed to this decorator (`@AuthorizerFilter('customMethodName')`). +The names of the generated CRUD resolver methods are similar to the ones of the [QueryService](../concepts/services.mdx): + +- `query` +- `findById` +- `aggregate` +- `updateOne` +- `updateMany` +- `deleteOne` +- `deleteMany` + +**Relations** + +- `query{PluralRelationName}` (e.g. querySubTasks) +- `find{SingularRelationName}` (e.g. findTodoItem) +- `aggregate{PluralRelationName}` (e.g. aggregateSubTasks) +- `remove{RelationName}from{SingularParentName}` (e.g. removeSubTaskFromTodoItem) +- `set{RelationName}On{SingularParentName}` (e.g. setSubTaskOnTodoItem) + ## Complete Example You can find a complete example in [`examples/auth`](https://github.com/doug-martin/nestjs-query/tree/master/examples/auth) diff --git a/examples/auth/e2e/todo-item.resolver.spec.ts b/examples/auth/e2e/todo-item.resolver.spec.ts index 6d2185efa..4e236fd5c 100644 --- a/examples/auth/e2e/todo-item.resolver.spec.ts +++ b/examples/auth/e2e/todo-item.resolver.spec.ts @@ -25,6 +25,7 @@ import { AuthService } from '../src/auth/auth.service'; describe('TodoItemResolver (auth - e2e)', () => { let app: INestApplication; let jwtToken: string; + let adminJwtToken: string; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ @@ -49,6 +50,7 @@ describe('TodoItemResolver (auth - e2e)', () => { beforeEach(async () => { const authService = app.get(AuthService); jwtToken = (await authService.login({ username: 'nestjs-query', id: 1 })).accessToken; + adminJwtToken = (await authService.login({ username: 'nestjs-query-3', id: 3 })).accessToken; }); afterAll(() => refresh(app.get(Connection))); @@ -96,6 +98,34 @@ describe('TodoItemResolver (auth - e2e)', () => { }); })); + it(`should find a users todo item by id`, () => + request(app.getHttpServer()) + .post('/graphql') + .auth(adminJwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `{ + todoItem(id: 1) { + ${todoItemFields} + } + }`, + }) + .expect(200) + .then(({ body }) => { + expect(body).toEqual({ + data: { + todoItem: { + id: '1', + title: 'Create Nest App', + completed: true, + description: null, + age: expect.any(Number), + }, + }, + }); + })); + it(`should return null if the todo item is not found`, () => request(app.getHttpServer()) .post('/graphql') @@ -328,6 +358,39 @@ describe('TodoItemResolver (auth - e2e)', () => { ]); })); + it(`should allow querying for all users`, () => + request(app.getHttpServer()) + .post('/graphql') + .auth(adminJwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `{ + todoItems(filter: { id: { in: [1, 2, 3] } }) { + ${pageInfoField} + ${edgeNodes(todoItemFields)} + totalCount + } + }`, + }) + .expect(200) + .then(({ body }) => { + const { edges, pageInfo, totalCount }: CursorConnectionType = body.data.todoItems; + expect(pageInfo).toEqual({ + endCursor: 'YXJyYXljb25uZWN0aW9uOjI=', + hasNextPage: false, + hasPreviousPage: false, + startCursor: 'YXJyYXljb25uZWN0aW9uOjA=', + }); + expect(totalCount).toBe(3); + expect(edges).toHaveLength(3); + expect(edges.map((e) => e.node)).toEqual([ + { id: '1', title: 'Create Nest App', completed: true, description: null, age: expect.any(Number) }, + { id: '2', title: 'Create Entity', completed: false, description: null, age: expect.any(Number) }, + { id: '3', title: 'Create Entity Service', completed: false, description: null, age: expect.any(Number) }, + ]); + })); + it(`should allow querying on subTasks`, () => request(app.getHttpServer()) .post('/graphql') @@ -553,6 +616,33 @@ describe('TodoItemResolver (auth - e2e)', () => { ]); })); + it(`should return a aggregate response for all users`, () => + request(app.getHttpServer()) + .post('/graphql') + .auth(adminJwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `{ + todoItemAggregate { + ${todoItemAggregateFields} + } + }`, + }) + .expect(200) + .then(({ body }) => { + const res: AggregateResponse[] = body.data.todoItemAggregate; + expect(res).toEqual([ + { + avg: { id: 8 }, + count: { completed: 15, created: 15, description: 0, id: 15, title: 15, updated: 15 }, + max: { description: null, id: '15', title: 'How to create item With Sub Tasks' }, + min: { description: null, id: '1', title: 'Add Todo Item Resolver' }, + sum: { id: 120 }, + }, + ]); + })); + it(`should allow filtering`, () => request(app.getHttpServer()) .post('/graphql') @@ -884,6 +974,32 @@ describe('TodoItemResolver (auth - e2e)', () => { expect(body.errors[0].message).toBe('Unable to find TodoItemEntity with id: 6'); })); + it('should not allow updating a todoItem that does not belong to the admin', () => + request(app.getHttpServer()) + .post('/graphql') + .auth(adminJwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `mutation { + updateOneTodoItem( + input: { + id: "6", + update: { title: "Should Not Update", completed: true } + } + ) { + id + title + completed + } + }`, + }) + .expect(200) + .then(({ body }) => { + expect(body.errors).toHaveLength(1); + expect(body.errors[0].message).toBe('Unable to find TodoItemEntity with id: 6'); + })); + it('should call the beforeUpdateOne hook', () => request(app.getHttpServer()) .post('/graphql') @@ -1044,6 +1160,32 @@ describe('TodoItemResolver (auth - e2e)', () => { }, })); + it('should not allow update records that do not belong to the admin', () => + request(app.getHttpServer()) + .post('/graphql') + .auth(adminJwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `mutation { + updateManyTodoItems( + input: { + filter: {id: { in: ["6", "7"]} }, + update: { title: "Should not update", completed: true } + } + ) { + updatedCount + } + }`, + }) + .expect(200, { + data: { + updateManyTodoItems: { + updatedCount: 0, + }, + }, + })); + it('should call the beforeUpdateMany hook when updating todoItem', () => request(app.getHttpServer()) .post('/graphql') @@ -1206,6 +1348,29 @@ describe('TodoItemResolver (auth - e2e)', () => { expect(body.errors[0].message).toContain('Unable to find TodoItemEntity with id: 6'); })); + it('should not allow deleting a todoItem that does not belong to the admin', () => + request(app.getHttpServer()) + .post('/graphql') + .auth(adminJwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `mutation { + deleteOneTodoItem( + input: { id: "6" } + ) { + id + title + completed + } + }`, + }) + .expect(200) + .then(({ body }) => { + expect(body.errors).toHaveLength(1); + expect(body.errors[0].message).toContain('Unable to find TodoItemEntity with id: 6'); + })); + it('should require an id', () => request(app.getHttpServer()) .post('/graphql') @@ -1301,6 +1466,31 @@ describe('TodoItemResolver (auth - e2e)', () => { }, })); + it('should not allow deleting multiple todoItems that do not belong to the admin', () => + request(app.getHttpServer()) + .post('/graphql') + .auth(adminJwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `mutation { + deleteManyTodoItems( + input: { + filter: {id: { in: ["6", "7"]} }, + } + ) { + deletedCount + } + }`, + }) + .expect(200, { + data: { + deleteManyTodoItems: { + deletedCount: 0, + }, + }, + })); + it('should require a filter', () => request(app.getHttpServer()) .post('/graphql') diff --git a/examples/auth/src/sub-task/sub-task.authorizer.ts b/examples/auth/src/sub-task/sub-task.authorizer.ts index 0df4cc014..542fd172d 100644 --- a/examples/auth/src/sub-task/sub-task.authorizer.ts +++ b/examples/auth/src/sub-task/sub-task.authorizer.ts @@ -4,7 +4,15 @@ import { UserContext } from '../auth/auth.interfaces'; import { SubTaskDTO } from './dto/sub-task.dto'; export class SubTaskAuthorizer implements Authorizer { - authorize(context: UserContext): Promise> { + authorize(context: UserContext, operationName?: string): Promise> { + if ( + context.req.user.username === 'nestjs-query-3' && + (operationName?.startsWith('query') || + operationName?.startsWith('find') || + operationName?.startsWith('aggregate')) + ) { + return Promise.resolve({}); + } return Promise.resolve({ ownerId: { eq: context.req.user.id } }); } diff --git a/examples/auth/src/todo-item/dto/todo-item.dto.ts b/examples/auth/src/todo-item/dto/todo-item.dto.ts index 886a430a7..3607b6210 100644 --- a/examples/auth/src/todo-item/dto/todo-item.dto.ts +++ b/examples/auth/src/todo-item/dto/todo-item.dto.ts @@ -13,7 +13,19 @@ import { UserContext } from '../../auth/auth.interfaces'; @ObjectType('TodoItem') @QueryOptions({ enableTotalCount: true }) -@Authorize({ authorize: (context: UserContext) => ({ ownerId: { eq: context.req.user.id } }) }) +@Authorize({ + authorize: (context: UserContext, operationName?: string) => { + if ( + context.req.user.username === 'nestjs-query-3' && + (operationName?.startsWith('query') || + operationName?.startsWith('find') || + operationName?.startsWith('aggregate')) + ) { + return {}; + } + return { ownerId: { eq: context.req.user.id } }; + }, +}) @Relation('owner', () => UserDTO, { disableRemove: true, disableUpdate: true }) @FilterableCursorConnection('subTasks', () => SubTaskDTO, { disableRemove: true }) @FilterableCursorConnection('tags', () => TagDTO) diff --git a/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts b/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts index 866296949..05d62013a 100644 --- a/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts +++ b/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts @@ -35,9 +35,17 @@ describe('createDefaultAuthorizer', () => { decoratorOwnerId!: number; } - @Authorize({ authorize: (ctx: UserContext) => ({ ownerId: { eq: ctx.user.id } }) }) + @Authorize({ + authorize: (ctx: UserContext, operationName?: string) => + operationName === 'other' ? { ownerId: { neq: ctx.user.id } } : { ownerId: { eq: ctx.user.id } }, + }) @Relation('relations', () => TestRelation, { - auth: { authorize: (ctx: UserContext) => ({ relationOwnerId: { eq: ctx.user.id } }) }, + auth: { + authorize: (ctx: UserContext, operationName?: string) => + operationName === 'other' + ? { relationOwnerId: { neq: ctx.user.id } } + : { relationOwnerId: { eq: ctx.user.id } }, + }, }) @UnPagedRelation('unPagedDecoratorRelations', () => TestDecoratorRelation) @Relation('authorizerRelation', () => RelationWithAuthorizer) @@ -71,6 +79,12 @@ describe('createDefaultAuthorizer', () => { expect(filter).toEqual({ ownerId: { eq: 2 } }); }); + it('should create an auth filter that depends on the passed operation name', async () => { + const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); + const filter = await authorizer.authorize({ user: { id: 2 } }, 'other'); + expect(filter).toEqual({ ownerId: { neq: 2 } }); + }); + it('should return an empty filter if auth not found', async () => { const authorizer = testingModule.get>(getAuthorizerToken(TestNoAuthDTO)); const filter = await authorizer.authorize({ user: { id: 2 } }); @@ -89,6 +103,12 @@ describe('createDefaultAuthorizer', () => { expect(filter).toEqual({ relationOwnerId: { eq: 2 } }); }); + it('should create an auth filter that depends on the passed operation name for relations using the relation options', async () => { + const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); + const filter = await authorizer.authorizeRelation('relations', { user: { id: 2 } }, 'other'); + expect(filter).toEqual({ relationOwnerId: { neq: 2 } }); + }); + it('should create an auth filter for relations using the relation authorizer', async () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); const filter = await authorizer.authorizeRelation('authorizerRelation', { user: { id: 2 } }); diff --git a/packages/query-graphql/src/auth/authorizer.ts b/packages/query-graphql/src/auth/authorizer.ts index ca25fa634..2cfb7d69b 100644 --- a/packages/query-graphql/src/auth/authorizer.ts +++ b/packages/query-graphql/src/auth/authorizer.ts @@ -2,8 +2,8 @@ import { Filter } from '@nestjs-query/core'; export interface Authorizer { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any - authorize(context: any): Promise>; + authorize(context: any, operationName?: string): Promise>; // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorizeRelation(relationName: string, context: any): Promise>; + authorizeRelation(relationName: string, context: any, operationName?: string): Promise>; } diff --git a/packages/query-graphql/src/auth/default-crud.authorizer.ts b/packages/query-graphql/src/auth/default-crud.authorizer.ts index 6e28006ab..0431aa45b 100644 --- a/packages/query-graphql/src/auth/default-crud.authorizer.ts +++ b/packages/query-graphql/src/auth/default-crud.authorizer.ts @@ -8,13 +8,13 @@ import { Authorizer } from './authorizer'; export interface AuthorizerOptions { // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorize: (context: any) => Filter | Promise>; + authorize: (context: any, operationName?: string) => Filter | Promise>; } const createRelationAuthorizer = (opts: AuthorizerOptions): Authorizer => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any - async authorize(context: any): Promise> { - return opts.authorize(context) ?? {}; + async authorize(context: any, operationName?: string): Promise> { + return opts.authorize(context, operationName) ?? {}; }, authorizeRelation(): Promise> { return Promise.reject(new Error('Not implemented')); @@ -43,13 +43,13 @@ export function createDefaultAuthorizer( } // eslint-disable-next-line @typescript-eslint/no-explicit-any - async authorize(context: any): Promise> { - return this.authOptions?.authorize(context) ?? {}; + async authorize(context: any, operationName?: string): Promise> { + return this.authOptions?.authorize(context, operationName) ?? {}; } // eslint-disable-next-line @typescript-eslint/no-explicit-any - async authorizeRelation(relationName: string, context: any): Promise> { - return this.relationsAuthorizers.get(relationName)?.authorize(context) ?? {}; + async authorizeRelation(relationName: string, context: any, operationName?: string): Promise> { + return this.relationsAuthorizers.get(relationName)?.authorize(context, operationName) ?? {}; } private get relations(): Map> { diff --git a/packages/query-graphql/src/decorators/authorize-filter.decorator.ts b/packages/query-graphql/src/decorators/authorize-filter.decorator.ts index cc6c74e17..942522827 100644 --- a/packages/query-graphql/src/decorators/authorize-filter.decorator.ts +++ b/packages/query-graphql/src/decorators/authorize-filter.decorator.ts @@ -8,40 +8,56 @@ function getContext(executionContext: ExecutionContext): C { return gqlExecutionContext.getContext(); } -function getAuthorizerFilter>(context: C) { +function getAuthorizerFilter>(context: C, operationName: string) { if (!context.authorizer) { return undefined; } - return context.authorizer.authorize(context); + return context.authorizer.authorize(context, operationName); } -function getRelationAuthFilter>(context: C, relationName: string) { +function getRelationAuthFilter>( + context: C, + relationName: string, + operationName: string, +) { if (!context.authorizer) { return undefined; } - return context.authorizer.authorizeRelation(relationName, context); + return context.authorizer.authorizeRelation(relationName, context, operationName); } -export function AuthorizerFilter(): ParameterDecorator { - return createParamDecorator((data: unknown, executionContext: ExecutionContext) => - getAuthorizerFilter(getContext>(executionContext)), - )(); +export function AuthorizerFilter(operationName?: string): ParameterDecorator { + // eslint-disable-next-line @typescript-eslint/ban-types + return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { + const name = operationName ?? propertyKey.toString(); + return createParamDecorator((data: unknown, executionContext: ExecutionContext) => + getAuthorizerFilter(getContext>(executionContext), name), + )()(target, propertyKey, parameterIndex); + }; } -export function RelationAuthorizerFilter(relationName: string): ParameterDecorator { - return createParamDecorator((data: unknown, executionContext: ExecutionContext) => - getRelationAuthFilter(getContext>(executionContext), relationName), - )(); +export function RelationAuthorizerFilter(relationName: string, operationName?: string): ParameterDecorator { + // eslint-disable-next-line @typescript-eslint/ban-types + return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { + const name = operationName ?? propertyKey.toString(); + return createParamDecorator((data: unknown, executionContext: ExecutionContext) => + getRelationAuthFilter(getContext>(executionContext), relationName, name), + )()(target, propertyKey, parameterIndex); + }; } -export function ModifyRelationAuthorizerFilter(relationName: string): ParameterDecorator { - return createParamDecorator( - async (data: unknown, executionContext: ExecutionContext): Promise> => { - const context = getContext>(executionContext); - return { - filter: await getAuthorizerFilter(context), - relationFilter: await getRelationAuthFilter(context, relationName), - }; - }, - )(); +export function ModifyRelationAuthorizerFilter(relationName: string, operationName?: string): ParameterDecorator { + // eslint-disable-next-line @typescript-eslint/ban-types + return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { + const name = operationName ?? propertyKey.toString(); + return createParamDecorator( + async (data: unknown, executionContext: ExecutionContext): Promise> => { + const context = getContext>(executionContext); + return { + filter: await getAuthorizerFilter(context, name), + relationFilter: await getRelationAuthFilter(context, relationName, name), + }; + }, + )()(target, propertyKey, parameterIndex); + }; } diff --git a/packages/query-graphql/src/resolvers/read.resolver.ts b/packages/query-graphql/src/resolvers/read.resolver.ts index 05e706715..6a5994834 100644 --- a/packages/query-graphql/src/resolvers/read.resolver.ts +++ b/packages/query-graphql/src/resolvers/read.resolver.ts @@ -86,7 +86,7 @@ export const Readable = , QS extends ) async queryMany( @HookArgs() query: QA, - @AuthorizerFilter() authorizeFilter?: Filter, + @AuthorizerFilter('query') authorizeFilter?: Filter, ): Promise> { return ConnectionType.createFromPromise( (q) => this.service.query(q),