From c63671e78d29dcedba3bfb7001b31388b808b822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20W=C3=B6lk?= Date: Wed, 7 Apr 2021 12:02:21 +0200 Subject: [PATCH] refactor(graphql,auth,#1026): made context in authorizer not optional --- examples/auth/e2e/todo-item.resolver.spec.ts | 22 ++++++++++++ .../auth/src/todo-item/todo-item.resolver.ts | 16 +++++++++ .../auth/default-crud-auth.service.spec.ts | 34 +++++++++++++++---- packages/query-graphql/src/auth/authorizer.ts | 4 +-- .../src/auth/default-crud.authorizer.ts | 8 ++--- .../decorators/authorize-filter.decorator.ts | 15 +++++--- 6 files changed, 83 insertions(+), 16 deletions(-) diff --git a/examples/auth/e2e/todo-item.resolver.spec.ts b/examples/auth/e2e/todo-item.resolver.spec.ts index 157ea4f95..a92cb62ee 100644 --- a/examples/auth/e2e/todo-item.resolver.spec.ts +++ b/examples/auth/e2e/todo-item.resolver.spec.ts @@ -498,6 +498,28 @@ describe('TodoItemResolver (auth - e2e)', () => { ]); })); + it(`should throw an error if AuthorizationContext was not setup`, () => + request(app.getHttpServer()) + .post('/graphql') + .auth(user3JwtToken, { type: 'bearer' }) + .send({ + operationName: null, + variables: {}, + query: `{ + failingTodoItems { + ${pageInfoField} + ${edgeNodes(todoItemFields)} + totalCount + } + }`, + }) + .expect(200) + .then(({ body }) => + expect(body.errors[0].message).toBe( + 'No AuthorizationContext available for method failingTodoItems! Make sure that you provide an AuthorizationContext to your custom methods as argument of the @AuthorizerFilter decorator.', + ), + )); + describe('paging', () => { it(`should allow paging with the 'first' field`, () => request(app.getHttpServer()) diff --git a/examples/auth/src/todo-item/todo-item.resolver.ts b/examples/auth/src/todo-item/todo-item.resolver.ts index b68d854e7..d4b0f0e00 100644 --- a/examples/auth/src/todo-item/todo-item.resolver.ts +++ b/examples/auth/src/todo-item/todo-item.resolver.ts @@ -54,4 +54,20 @@ export class TodoItemResolver { (q) => this.service.count(q), ); } + + @Query(() => TodoItemConnection) + async failingTodoItems( + @Args() query: TodoItemQuery, + @AuthorizerFilter() // Intentionally left out argument to test error + authFilter: Filter, + ): Promise> { + // add the completed filter the user provided filter + const filter: Filter = mergeFilter(query.filter ?? {}, { completed: { is: false } }); + const uncompletedQuery = mergeQuery(query, { filter: mergeFilter(filter, authFilter) }); + return TodoItemConnection.createFromPromise( + (q) => this.service.query(q), + uncompletedQuery, + (q) => this.service.count(q), + ); + } } 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 81fed7af4..153ea261b 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 @@ -77,7 +77,10 @@ describe('createDefaultAuthorizer', () => { it('should create an auth filter', async () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); - const filter = await authorizer.authorize({ user: { id: 2 } }); + const filter = await authorizer.authorize( + { user: { id: 2 } }, + { operationName: 'queryMany', operationGroup: OperationGroup.READ, readonly: true, many: true }, + ); expect(filter).toEqual({ ownerId: { eq: 2 } }); }); @@ -92,19 +95,30 @@ describe('createDefaultAuthorizer', () => { 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 } }); + const filter = await authorizer.authorize( + { user: { id: 2 } }, + { operationName: 'queryMany', operationGroup: OperationGroup.READ, readonly: true, many: true }, + ); expect(filter).toEqual({}); }); it('should create an auth filter for relations using the default auth decorator', async () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); - const filter = await authorizer.authorizeRelation('unPagedDecoratorRelations', { user: { id: 2 } }); + const filter = await authorizer.authorizeRelation( + 'unPagedDecoratorRelations', + { user: { id: 2 } }, + { operationName: 'queryRelation', operationGroup: OperationGroup.READ, readonly: true, many: true }, + ); expect(filter).toEqual({ decoratorOwnerId: { eq: 2 } }); }); it('should create an auth filter for relations using the relation options', async () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); - const filter = await authorizer.authorizeRelation('relations', { user: { id: 2 } }); + const filter = await authorizer.authorizeRelation( + 'relations', + { user: { id: 2 } }, + { operationName: 'queryRelation', operationGroup: OperationGroup.READ, readonly: true, many: true }, + ); expect(filter).toEqual({ relationOwnerId: { eq: 2 } }); }); @@ -120,13 +134,21 @@ describe('createDefaultAuthorizer', () => { 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 } }); + const filter = await authorizer.authorizeRelation( + 'authorizerRelation', + { user: { id: 2 } }, + { operationName: 'queryRelation', operationGroup: OperationGroup.READ, readonly: true, many: true }, + ); expect(filter).toEqual({ authorizerOwnerId: { eq: 2 } }); }); it('should return an empty object for an unknown relation', async () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); - const filter = await authorizer.authorizeRelation('unknownRelations', { user: { id: 2 } }); + const filter = await authorizer.authorizeRelation( + 'unknownRelations', + { user: { id: 2 } }, + { operationName: 'queryRelation', operationGroup: OperationGroup.READ, readonly: true, many: true }, + ); expect(filter).toEqual({}); }); }); diff --git a/packages/query-graphql/src/auth/authorizer.ts b/packages/query-graphql/src/auth/authorizer.ts index 96c90bff5..81bebadf8 100644 --- a/packages/query-graphql/src/auth/authorizer.ts +++ b/packages/query-graphql/src/auth/authorizer.ts @@ -24,12 +24,12 @@ export interface AuthorizationContext { export interface Authorizer { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any - authorize(context: any, authorizerContext?: AuthorizationContext): Promise>; + authorize(context: any, authorizerContext: AuthorizationContext): Promise>; authorizeRelation( relationName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any context: any, - authorizerContext?: AuthorizationContext, + authorizerContext: AuthorizationContext, ): Promise>; } diff --git a/packages/query-graphql/src/auth/default-crud.authorizer.ts b/packages/query-graphql/src/auth/default-crud.authorizer.ts index d64e702f4..f6b47064c 100644 --- a/packages/query-graphql/src/auth/default-crud.authorizer.ts +++ b/packages/query-graphql/src/auth/default-crud.authorizer.ts @@ -8,12 +8,12 @@ import { Authorizer, AuthorizationContext } from './authorizer'; export interface AuthorizerOptions { // eslint-disable-next-line @typescript-eslint/no-explicit-any - authorize: (context: any, authorizationContext?: AuthorizationContext) => Filter | Promise>; + authorize: (context: any, authorizationContext: AuthorizationContext) => Filter | Promise>; } const createRelationAuthorizer = (opts: AuthorizerOptions): Authorizer => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any - async authorize(context: any, authorizationContext?: AuthorizationContext): Promise> { + async authorize(context: any, authorizationContext: AuthorizationContext): Promise> { return opts.authorize(context, authorizationContext) ?? {}; }, authorizeRelation(): Promise> { @@ -43,7 +43,7 @@ export function createDefaultAuthorizer( } // eslint-disable-next-line @typescript-eslint/no-explicit-any - async authorize(context: any, authorizationContext?: AuthorizationContext): Promise> { + async authorize(context: any, authorizationContext: AuthorizationContext): Promise> { return this.authOptions?.authorize(context, authorizationContext) ?? {}; } @@ -51,7 +51,7 @@ export function createDefaultAuthorizer( relationName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any context: any, - authorizationContext?: AuthorizationContext, + authorizationContext: AuthorizationContext, ): Promise> { return this.relationsAuthorizers.get(relationName)?.authorize(context, authorizationContext) ?? {}; } diff --git a/packages/query-graphql/src/decorators/authorize-filter.decorator.ts b/packages/query-graphql/src/decorators/authorize-filter.decorator.ts index 14d1bcc11..76b2eeee3 100644 --- a/packages/query-graphql/src/decorators/authorize-filter.decorator.ts +++ b/packages/query-graphql/src/decorators/authorize-filter.decorator.ts @@ -14,7 +14,7 @@ function getContext(executionContext: ExecutionContext): C { function getAuthorizerFilter>( context: C, - authorizationContext?: AuthorizationContext, + authorizationContext: AuthorizationContext, ) { if (!context.authorizer) { return undefined; @@ -25,7 +25,7 @@ function getAuthorizerFilter>( function getRelationAuthFilter>( context: C, relationName: string, - authorizationContext?: AuthorizationContext, + authorizationContext: AuthorizationContext, ) { if (!context.authorizer) { return undefined; @@ -36,8 +36,15 @@ function getRelationAuthFilter>( function getAuthorizationContext( methodName: string | symbol, partialAuthContext?: PartialAuthorizationContext, -): AuthorizationContext | undefined { - if (!partialAuthContext) return undefined; +): AuthorizationContext { + if (!partialAuthContext) + return new Proxy({} as AuthorizationContext, { + get: () => { + throw new Error( + `No AuthorizationContext available for method ${methodName.toString()}! Make sure that you provide an AuthorizationContext to your custom methods as argument of the @AuthorizerFilter decorator.`, + ); + }, + }); return { operationName: methodName.toString(),