Skip to content

Commit

Permalink
refactor(graphql,auth,#1026): made context in authorizer not optional
Browse files Browse the repository at this point in the history
  • Loading branch information
mwoelk authored and doug-martin committed Apr 9, 2021
1 parent 24fdb9e commit c63671e
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 16 deletions.
22 changes: 22 additions & 0 deletions examples/auth/e2e/todo-item.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
16 changes: 16 additions & 0 deletions examples/auth/src/todo-item/todo-item.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TodoItemDTO>,
): Promise<ConnectionType<TodoItemDTO>> {
// add the completed filter the user provided filter
const filter: Filter<TodoItemDTO> = 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),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ describe('createDefaultAuthorizer', () => {

it('should create an auth filter', async () => {
const authorizer = testingModule.get<Authorizer<TestDTO>>(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 } });
});

Expand All @@ -92,19 +95,30 @@ describe('createDefaultAuthorizer', () => {

it('should return an empty filter if auth not found', async () => {
const authorizer = testingModule.get<Authorizer<TestNoAuthDTO>>(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<Authorizer<TestDTO>>(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<Authorizer<TestDTO>>(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 } });
});

Expand All @@ -120,13 +134,21 @@ describe('createDefaultAuthorizer', () => {

it('should create an auth filter for relations using the relation authorizer', async () => {
const authorizer = testingModule.get<Authorizer<TestDTO>>(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<Authorizer<TestDTO>>(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({});
});
});
4 changes: 2 additions & 2 deletions packages/query-graphql/src/auth/authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ export interface AuthorizationContext {

export interface Authorizer<DTO> {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any
authorize(context: any, authorizerContext?: AuthorizationContext): Promise<Filter<DTO>>;
authorize(context: any, authorizerContext: AuthorizationContext): Promise<Filter<DTO>>;

authorizeRelation(
relationName: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: any,
authorizerContext?: AuthorizationContext,
authorizerContext: AuthorizationContext,
): Promise<Filter<unknown>>;
}
8 changes: 4 additions & 4 deletions packages/query-graphql/src/auth/default-crud.authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { Authorizer, AuthorizationContext } from './authorizer';

export interface AuthorizerOptions<DTO> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
authorize: (context: any, authorizationContext?: AuthorizationContext) => Filter<DTO> | Promise<Filter<DTO>>;
authorize: (context: any, authorizationContext: AuthorizationContext) => Filter<DTO> | Promise<Filter<DTO>>;
}

const createRelationAuthorizer = (opts: AuthorizerOptions<unknown>): Authorizer<unknown> => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async authorize(context: any, authorizationContext?: AuthorizationContext): Promise<Filter<unknown>> {
async authorize(context: any, authorizationContext: AuthorizationContext): Promise<Filter<unknown>> {
return opts.authorize(context, authorizationContext) ?? {};
},
authorizeRelation(): Promise<Filter<unknown>> {
Expand Down Expand Up @@ -43,15 +43,15 @@ export function createDefaultAuthorizer<DTO>(
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async authorize(context: any, authorizationContext?: AuthorizationContext): Promise<Filter<DTO>> {
async authorize(context: any, authorizationContext: AuthorizationContext): Promise<Filter<DTO>> {
return this.authOptions?.authorize(context, authorizationContext) ?? {};
}

async authorizeRelation(
relationName: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: any,
authorizationContext?: AuthorizationContext,
authorizationContext: AuthorizationContext,
): Promise<Filter<unknown>> {
return this.relationsAuthorizers.get(relationName)?.authorize(context, authorizationContext) ?? {};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function getContext<C>(executionContext: ExecutionContext): C {

function getAuthorizerFilter<C extends AuthorizerContext<unknown>>(
context: C,
authorizationContext?: AuthorizationContext,
authorizationContext: AuthorizationContext,
) {
if (!context.authorizer) {
return undefined;
Expand All @@ -25,7 +25,7 @@ function getAuthorizerFilter<C extends AuthorizerContext<unknown>>(
function getRelationAuthFilter<C extends AuthorizerContext<unknown>>(
context: C,
relationName: string,
authorizationContext?: AuthorizationContext,
authorizationContext: AuthorizationContext,
) {
if (!context.authorizer) {
return undefined;
Expand All @@ -36,8 +36,15 @@ function getRelationAuthFilter<C extends AuthorizerContext<unknown>>(
function getAuthorizationContext(
methodName: string | symbol,
partialAuthContext?: PartialAuthorizationContext,
): AuthorizationContext | undefined {
if (!partialAuthContext) return undefined;
): AuthorizationContext {
if (!partialAuthContext)
return new Proxy<AuthorizationContext>({} 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(),
Expand Down

0 comments on commit c63671e

Please sign in to comment.