Skip to content

Commit

Permalink
feat(api/guards): renamed RejectNestedCreateGuard to ForbidNestedCrea…
Browse files Browse the repository at this point in the history
…teGuard & added options

Created options for ForbidNestedCreateGuard of type { allow: string[] } to list fields to allow
nested creates while forbidding all other nested creates within mutation arguments.  This maximizes
flexibility to describe what is safe to expose on the API surface layer.

BREAKING CHANGE: Renamed RejectNestedCreateGuard to ForbidNestedCreateGuard
  • Loading branch information
ZenSoftware committed May 4, 2022
1 parent 020edea commit 9744b8c
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 115 deletions.
20 changes: 2 additions & 18 deletions apps/api/src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,11 @@ import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '../jwt';
import { PrismaModule } from '../prisma';
import { AuthService } from './auth.service';
import { GqlGuard, GqlThrottlerGuard, HttpGuard, RejectNestedCreateGuard } from './guards';
import { JwtStrategy } from './jwt.strategy';

@Module({
imports: [JwtModule, PrismaModule, PassportModule.register({ defaultStrategy: 'jwt' })],
providers: [
JwtStrategy,
AuthService,
GqlGuard,
GqlThrottlerGuard,
HttpGuard,
RejectNestedCreateGuard,
],
exports: [
JwtModule,
PassportModule,
AuthService,
GqlGuard,
GqlThrottlerGuard,
HttpGuard,
RejectNestedCreateGuard,
],
providers: [JwtStrategy, AuthService],
exports: [JwtModule, PassportModule, AuthService],
})
export class ZenAuthModule {}
28 changes: 28 additions & 0 deletions apps/api/src/app/auth/guards/forbid-nested-create.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { containsNestedCreate } from './forbid-nested-create.guard';

describe('ForbidNestedCreateGuard', () => {
it(`determines if args contains a "create" field`, () => {
const args1 = {
data: {
text: 'sample',
author: { connect: { id: 1 } },
comment: { create: { text: 'commenting' } },
},
};
expect(containsNestedCreate(args1)).toEqual(true);
expect(containsNestedCreate(args1, { allow: ['comment'] })).toEqual(false);

const args2 = {
data: {
stub: null,
stub2: undefined,
author: { create: { username: 'user1' } },
comment: { create: { text: 'commenting' } },
},
};
expect(containsNestedCreate(args2)).toEqual(true);
expect(containsNestedCreate(args2, { allow: ['author'] })).toEqual(true);
expect(containsNestedCreate(args2, { allow: ['comment'] })).toEqual(true);
expect(containsNestedCreate(args2, { allow: ['author', 'comment'] })).toEqual(false);
});
});
63 changes: 63 additions & 0 deletions apps/api/src/app/auth/guards/forbid-nested-create.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { CanActivate, ExecutionContext, HttpException, Logger, mixin } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

type Options = { allow: string[] };

export function containsNestedCreate(args: any, options: Options = undefined) {
if (args !== null && args !== undefined) {
for (const [key, value] of Object.entries(args)) {
if (options?.allow.includes(key)) {
continue;
}

if (key === 'create') {
return true;
}

if (typeof value === 'object' && containsNestedCreate(value, options) === true) {
return true;
}
}
}

return false;
}

/**
* Rejects mutations with nested create arguments
*/
export const ForbidNestedCreateGuard = (options: Options = undefined) => {
class ForbidNestedCreateGuardMixin implements CanActivate {
async canActivate(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);

if (ctx.getInfo()?.operation?.operation === 'mutation') {
const args = ctx.getArgs();

if (containsNestedCreate(args, options)) {
const errorMessage = 'Nested create arguments for mutations are forbidden';
const req = ctx.getContext()?.req;

Logger.error(errorMessage, {
userId: req?.user?.id,
ip: req?.ip,
class: ctx.getClass()?.name,
handler: ctx.getHandler()?.name,
args: args?.data,
});

throw new HttpException(errorMessage, 403);
}
}

return true;
}

getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}

return mixin(ForbidNestedCreateGuardMixin);
};
4 changes: 2 additions & 2 deletions apps/api/src/app/auth/guards/gql.guard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
Expand All @@ -12,7 +12,7 @@ import { authLogic } from './auth-logic';
* Imitates RBAC rules for [ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-6.0).
* **Super** users are granted unrestricted access.
*/
export class GqlGuard extends AuthGuard('jwt') implements CanActivate {
export class GqlGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app/auth/guards/http.guard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Role } from '@prisma/client';
Expand All @@ -11,7 +11,7 @@ import { authLogic } from './auth-logic';
* Imitates RBAC rules for [ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-6.0).
* **Super** users are granted unrestricted access.
*/
export class HttpGuard extends AuthGuard('jwt') implements CanActivate {
export class HttpGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/auth/guards/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './gql-throttle.guard';
export * from './gql.guard';
export * from './http.guard';
export { RejectNestedCreateGuard } from './reject-nested-create.guard';
export { ForbidNestedCreateGuard } from './forbid-nested-create.guard';
36 changes: 0 additions & 36 deletions apps/api/src/app/auth/guards/reject-nested-create.guard.spec.ts

This file was deleted.

51 changes: 0 additions & 51 deletions apps/api/src/app/auth/guards/reject-nested-create.guard.ts

This file was deleted.

4 changes: 2 additions & 2 deletions apps/api/src/app/graphql/resolvers/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Resolver,
} from '@nestjs/graphql';

import { GqlGuard, RejectNestedCreateGuard, Roles } from '../../auth';
import { ForbidNestedCreateGuard, GqlGuard, Roles } from '../../auth';
import { PrismaSelectArgs } from '../../prisma';
import resolvers from '../generated/User/resolvers';

Expand All @@ -28,7 +28,7 @@ export const typeDefs = null;
// `;

@Resolver('User')
@UseGuards(GqlGuard, RejectNestedCreateGuard)
@UseGuards(GqlGuard, ForbidNestedCreateGuard())
@Roles('Super')
export class UserResolver {
@ResolveField()
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zen",
"version": "4.1.0",
"version": "4.2.0",
"license": "MIT",
"private": true,
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions tools/graphql-codegen/nest-resolvers.temp.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = (prismaName, querySource, mutationSource) => {
return `import { UseGuards } from '@nestjs/common';
import { Args, Context, Info, Mutation, Parent, Query, Resolver } from '@nestjs/graphql';
import { GqlGuard, RejectNestedCreateGuard, Roles } from '../../auth';
import { GqlGuard, ForbidNestedCreateGuard, Roles } from '../../auth';
import { PrismaSelectArgs } from '../../prisma';
import resolvers from '../generated/${prismaName}/resolvers';
Expand All @@ -20,7 +20,7 @@ export const typeDefs = null;
// \`;
@Resolver('${prismaName}')
@UseGuards(GqlGuard, RejectNestedCreateGuard)
@UseGuards(GqlGuard, ForbidNestedCreateGuard())
@Roles('Super')
export class ${prismaName}Resolver {
${querySource}${mutationSource}
Expand Down

0 comments on commit 9744b8c

Please sign in to comment.