Skip to content

Commit

Permalink
feat(graphql,auth,#1026): Enable authorization on create methods as well
Browse files Browse the repository at this point in the history
  • Loading branch information
mwoelk authored and doug-martin committed Apr 13, 2021
1 parent 32df50e commit 4c7905e
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 20 deletions.
51 changes: 38 additions & 13 deletions examples/auth/e2e/todo-item.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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)));
Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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: {},
Expand Down
4 changes: 4 additions & 0 deletions examples/auth/src/todo-item/dto/todo-item.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 } };
},
})
Expand Down
28 changes: 21 additions & 7 deletions packages/query-graphql/src/resolvers/create.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -127,10 +127,17 @@ export const Creatable = <DTO, C, QS extends QueryService<DTO, C, unknown>>(
() => 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<DTO> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async createOne(@MutationHookArgs() input: CO, @AuthorizerFilter() authorizeFilter?: Filter<DTO>): Promise<DTO> {
// 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);
Expand All @@ -142,10 +149,17 @@ export const Creatable = <DTO, C, QS extends QueryService<DTO, C, unknown>>(
() => [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<DTO[]> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async createMany(@MutationHookArgs() input: CM, @AuthorizerFilter() authorizeFilter?: Filter<DTO>): Promise<DTO[]> {
// 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)));
Expand Down

0 comments on commit 4c7905e

Please sign in to comment.