From 8ae94590b460d445098f39212ac83c6dfabea4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20W=C3=B6lk?= Date: Mon, 5 Apr 2021 17:13:03 +0200 Subject: [PATCH] feat(graphql,auth,#1026): Enable authorization on create methods as well --- examples/auth/e2e/todo-item.resolver.spec.ts | 51 ++++++++++++++----- .../auth/src/todo-item/dto/todo-item.dto.ts | 4 ++ .../src/resolvers/create.resolver.ts | 28 +++++++--- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/examples/auth/e2e/todo-item.resolver.spec.ts b/examples/auth/e2e/todo-item.resolver.spec.ts index 4e236fd5c..157ea4f95 100644 --- a/examples/auth/e2e/todo-item.resolver.spec.ts +++ b/examples/auth/e2e/todo-item.resolver.spec.ts @@ -25,7 +25,7 @@ import { AuthService } from '../src/auth/auth.service'; describe('TodoItemResolver (auth - e2e)', () => { let app: INestApplication; let jwtToken: string; - let adminJwtToken: string; + let user3JwtToken: string; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ @@ -50,7 +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; + user3JwtToken = (await authService.login({ username: 'nestjs-query-3', id: 3 })).accessToken; }); afterAll(() => refresh(app.get(Connection))); @@ -101,7 +101,7 @@ describe('TodoItemResolver (auth - e2e)', () => { it(`should find a users todo item by id`, () => request(app.getHttpServer()) .post('/graphql') - .auth(adminJwtToken, { type: 'bearer' }) + .auth(user3JwtToken, { type: 'bearer' }) .send({ operationName: null, variables: {}, @@ -361,7 +361,7 @@ describe('TodoItemResolver (auth - e2e)', () => { it(`should allow querying for all users`, () => request(app.getHttpServer()) .post('/graphql') - .auth(adminJwtToken, { type: 'bearer' }) + .auth(user3JwtToken, { type: 'bearer' }) .send({ operationName: null, variables: {}, @@ -619,7 +619,7 @@ describe('TodoItemResolver (auth - e2e)', () => { it(`should return a aggregate response for all users`, () => request(app.getHttpServer()) .post('/graphql') - .auth(adminJwtToken, { type: 'bearer' }) + .auth(user3JwtToken, { type: 'bearer' }) .send({ operationName: null, variables: {}, @@ -723,6 +723,31 @@ describe('TodoItemResolver (auth - e2e)', () => { }, })); + it('should forbid creating a todoItem for user 3', () => + request(app.getHttpServer()) + .post('/graphql') + .auth(user3JwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `mutation { + createOneTodoItem( + input: { + todoItem: { title: "Test Todo", completed: false } + } + ) { + id + title + completed + } + }`, + }) + .expect(200) + .then(({ body }) => { + expect(body.errors).toHaveLength(1); + expect(JSON.stringify(body.errors[0])).toContain('Unauthorized'); + })); + it('should call the beforeCreateOne hook', () => request(app.getHttpServer()) .post('/graphql') @@ -974,10 +999,10 @@ 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', () => + it('should not allow updating a todoItem that does not belong to user 3', () => request(app.getHttpServer()) .post('/graphql') - .auth(adminJwtToken, { type: 'bearer' }) + .auth(user3JwtToken, { type: 'bearer' }) .send({ operationName: null, variables: {}, @@ -1160,10 +1185,10 @@ describe('TodoItemResolver (auth - e2e)', () => { }, })); - it('should not allow update records that do not belong to the admin', () => + it('should not allow update records that do not belong to user 3', () => request(app.getHttpServer()) .post('/graphql') - .auth(adminJwtToken, { type: 'bearer' }) + .auth(user3JwtToken, { type: 'bearer' }) .send({ operationName: null, variables: {}, @@ -1348,10 +1373,10 @@ 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', () => + it('should not allow deleting a todoItem that does not belong to user 3', () => request(app.getHttpServer()) .post('/graphql') - .auth(adminJwtToken, { type: 'bearer' }) + .auth(user3JwtToken, { type: 'bearer' }) .send({ operationName: null, variables: {}, @@ -1466,10 +1491,10 @@ describe('TodoItemResolver (auth - e2e)', () => { }, })); - it('should not allow deleting multiple todoItems that do not belong to the admin', () => + it('should not allow deleting multiple todoItems that do not belong to user 3', () => request(app.getHttpServer()) .post('/graphql') - .auth(adminJwtToken, { type: 'bearer' }) + .auth(user3JwtToken, { type: 'bearer' }) .send({ operationName: null, variables: {}, 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 235fa9c02..9b38eb2b6 100644 --- a/examples/auth/src/todo-item/dto/todo-item.dto.ts +++ b/examples/auth/src/todo-item/dto/todo-item.dto.ts @@ -7,6 +7,7 @@ import { AuthorizationContext, } from '@nestjs-query/query-graphql'; import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql'; +import { UnauthorizedException } from '@nestjs/common'; import { SubTaskDTO } from '../../sub-task/dto/sub-task.dto'; import { TagDTO } from '../../tag/dto/tag.dto'; import { UserDTO } from '../../user/user.dto'; @@ -22,6 +23,9 @@ import { UserContext } from '../../auth/auth.interfaces'; ) { return {}; } + if (context.req.user.username === 'nestjs-query-3' && authorizationContext?.operationGroup === 'create') { + throw new UnauthorizedException(); + } return { ownerId: { eq: context.req.user.id } }; }, }) diff --git a/packages/query-graphql/src/resolvers/create.resolver.ts b/packages/query-graphql/src/resolvers/create.resolver.ts index 7fc5c4ef1..af2b83fe4 100644 --- a/packages/query-graphql/src/resolvers/create.resolver.ts +++ b/packages/query-graphql/src/resolvers/create.resolver.ts @@ -3,13 +3,13 @@ * @packageDocumentation */ // eslint-disable-next-line max-classes-per-file -import { Class, DeepPartial, QueryService } from '@nestjs-query/core'; +import { Class, DeepPartial, Filter, QueryService } from '@nestjs-query/core'; import { Args, ArgsType, InputType, PartialType, Resolver } from '@nestjs/graphql'; import omit from 'lodash.omit'; import { HookTypes } from '../hooks'; import { DTONames, getDTONames } from '../common'; -import { MutationHookArgs, ResolverMutation, ResolverSubscription } from '../decorators'; -import { HookInterceptor } from '../interceptors'; +import { AuthorizerFilter, MutationHookArgs, ResolverMutation, ResolverSubscription } from '../decorators'; +import { AuthorizerInterceptor, HookInterceptor } from '../interceptors'; import { EventType, getDTOEventName } from '../subscription'; import { CreateManyInputType, @@ -127,10 +127,17 @@ export const Creatable = >( () => DTOClass, { name: createOneMutationName }, commonResolverOpts, - { interceptors: [HookInterceptor(HookTypes.BEFORE_CREATE_ONE, CreateDTOClass, DTOClass)] }, + { + interceptors: [ + HookInterceptor(HookTypes.BEFORE_CREATE_ONE, CreateDTOClass, DTOClass), + AuthorizerInterceptor(DTOClass), + ], + }, opts.one ?? {}, ) - async createOne(@MutationHookArgs() input: CO): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async createOne(@MutationHookArgs() input: CO, @AuthorizerFilter() authorizeFilter?: Filter): Promise { + // 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); @@ -142,10 +149,17 @@ export const Creatable = >( () => [DTOClass], { name: createManyMutationName }, { ...commonResolverOpts }, - { interceptors: [HookInterceptor(HookTypes.BEFORE_CREATE_MANY, CreateDTOClass, DTOClass)] }, + { + interceptors: [ + HookInterceptor(HookTypes.BEFORE_CREATE_MANY, CreateDTOClass, DTOClass), + AuthorizerInterceptor(DTOClass), + ], + }, opts.many ?? {}, ) - async createMany(@MutationHookArgs() input: CM): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async createMany(@MutationHookArgs() input: CM, @AuthorizerFilter() authorizeFilter?: Filter): Promise { + // 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)));