diff --git a/apps/api/src/app/auth/auth-logic.ts b/apps/api/src/app/auth/auth-logic.ts new file mode 100644 index 000000000..cfdc9c0b7 --- /dev/null +++ b/apps/api/src/app/auth/auth-logic.ts @@ -0,0 +1,25 @@ +import { Role } from '@prisma/client'; + +type RoleType = string[] | Role[] | undefined; + +export function authLogic( + userRoles: RoleType, + classRoles: RoleType, + handlerRoles: RoleType +): boolean { + const _userRoles = userRoles ?? []; + const _classRoles = classRoles ?? []; + const _handlerRoles = handlerRoles ?? []; + + // Give super users unlimited access + if (_userRoles.includes(Role.Super)) return true; + + if (_classRoles.length > 0) { + if (!_userRoles.some(r => _classRoles.includes(r as Role))) return false; + if (_handlerRoles.length > 0 && !_userRoles.some(r => _handlerRoles.includes(r))) return false; + } else if (_handlerRoles.length > 0 && !_userRoles.some(r => _handlerRoles.includes(r))) { + return false; + } + + return true; +} diff --git a/apps/api/src/app/auth/auth.module.ts b/apps/api/src/app/auth/auth.module.ts index ad212c34f..469687222 100644 --- a/apps/api/src/app/auth/auth.module.ts +++ b/apps/api/src/app/auth/auth.module.ts @@ -4,7 +4,7 @@ import { PassportModule } from '@nestjs/passport'; import { JwtModule } from '../jwt'; import { PrismaModule } from '../prisma'; import { AuthService } from './auth.service'; -import { GqlGuard } from './gql'; +import { GqlGuard } from './gql.guard'; import { JwtStrategy } from './jwt.strategy'; @Module({ diff --git a/apps/api/src/app/auth/gql/gql-user.decorator.ts b/apps/api/src/app/auth/gql-user.decorator.ts similarity index 89% rename from apps/api/src/app/auth/gql/gql-user.decorator.ts rename to apps/api/src/app/auth/gql-user.decorator.ts index 3d943d0f7..9635e0bd7 100644 --- a/apps/api/src/app/auth/gql/gql-user.decorator.ts +++ b/apps/api/src/app/auth/gql-user.decorator.ts @@ -1,7 +1,7 @@ import { ExecutionContext, UnauthorizedException, createParamDecorator } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; -import { RequestUser } from '../request-user'; +import { RequestUser } from './request-user'; export const GqlUser = createParamDecorator((data, context: ExecutionContext) => { const user = GqlExecutionContext.create(context).getContext().req.user; diff --git a/apps/api/src/app/auth/gql.guard.ts b/apps/api/src/app/auth/gql.guard.ts new file mode 100644 index 000000000..bfdcb49b9 --- /dev/null +++ b/apps/api/src/app/auth/gql.guard.ts @@ -0,0 +1,30 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { AuthGuard } from '@nestjs/passport'; +import { Role } from '@prisma/client'; + +import { authLogic } from './auth-logic'; +import { ROLES_KEY } from './roles.decorator'; + +@Injectable() +/** + * Replicates 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 unlimited access. + */ +export class GqlGuard extends AuthGuard('jwt') { + constructor(private readonly reflector: Reflector) { + super(); + } + + async canActivate(context: ExecutionContext) { + await super.canActivate(context); + + const ctx = GqlExecutionContext.create(context); + const user = ctx.getContext().req.user; + const classRoles = this.reflector.get(ROLES_KEY, ctx.getClass()); + const handlerRoles = this.reflector.get(ROLES_KEY, ctx.getHandler()); + + return authLogic(user.roles, classRoles, handlerRoles); + } +} diff --git a/apps/api/src/app/auth/gql/gql.guard.ts b/apps/api/src/app/auth/gql/gql.guard.ts deleted file mode 100644 index 724b519e3..000000000 --- a/apps/api/src/app/auth/gql/gql.guard.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ExecutionContext, Injectable } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { GqlExecutionContext } from '@nestjs/graphql'; -import { AuthGuard } from '@nestjs/passport'; -import { Role } from '@prisma/client'; - -@Injectable() -/** - * Replicates 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 unlimited access. - */ -export class GqlGuard extends AuthGuard('jwt') { - constructor(private readonly reflector: Reflector) { - super(); - } - - async canActivate(context: ExecutionContext) { - await super.canActivate(context); - - const ctx = GqlExecutionContext.create(context); - const user = ctx.getContext().req.user; - const userRoles: Role[] = user.roles ?? []; - - // Give super users unlimited access - if (userRoles.includes(Role.Super)) return true; - - let classRoles = this.reflector.get('roles', ctx.getClass()); - classRoles = classRoles ?? []; - - let handlerRoles = this.reflector.get('roles', ctx.getHandler()); - handlerRoles = handlerRoles ?? []; - - if (classRoles.length > 0) { - if (!userRoles.some(r => classRoles.includes(r))) return false; - if (handlerRoles.length > 0 && !userRoles.some(r => handlerRoles.includes(r))) return false; - } else if (handlerRoles.length > 0 && !userRoles.some(r => handlerRoles.includes(r))) { - return false; - } - - return true; - } - - getRequest(context: ExecutionContext) { - const ctx = GqlExecutionContext.create(context); - return ctx.getContext().req; - } -} diff --git a/apps/api/src/app/auth/gql/index.ts b/apps/api/src/app/auth/gql/index.ts deleted file mode 100644 index 700f9e287..000000000 --- a/apps/api/src/app/auth/gql/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './gql.guard'; -export * from './gql-user.decorator'; diff --git a/apps/api/src/app/auth/http.guard.ts b/apps/api/src/app/auth/http.guard.ts new file mode 100644 index 000000000..03e31da6d --- /dev/null +++ b/apps/api/src/app/auth/http.guard.ts @@ -0,0 +1,24 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { Role } from '@prisma/client'; + +import { authLogic } from './auth-logic'; +import { ROLES_KEY } from './roles.decorator'; + +@Injectable() +export class HttpGuard extends AuthGuard('jwt') { + constructor(private readonly reflector: Reflector) { + super(); + } + + async canActivate(ctx: ExecutionContext) { + await super.canActivate(ctx); + + const { user } = ctx.switchToHttp().getRequest(); + const classRoles = this.reflector.get(ROLES_KEY, ctx.getClass()); + const handlerRoles = this.reflector.get(ROLES_KEY, ctx.getHandler()); + + return authLogic(user.roles, classRoles, handlerRoles); + } +} diff --git a/apps/api/src/app/auth/index.ts b/apps/api/src/app/auth/index.ts index 93c694533..0d894bde8 100644 --- a/apps/api/src/app/auth/index.ts +++ b/apps/api/src/app/auth/index.ts @@ -1,8 +1,10 @@ export { AuthGuard } from '@nestjs/passport'; -export * from './auth.module'; -export * from './gql'; +export { Role } from '@prisma/client'; export { RequestUser } from './request-user'; -export * from './roles.decorator'; +export * from './auth.module'; export * from './auth.service'; +export * from './gql-user.decorator'; +export * from './gql.guard'; export * from './http-user.decorator'; -export { Role } from '@prisma/client'; +export * from './http.guard'; +export * from './roles.decorator'; diff --git a/apps/api/src/app/auth/roles.decorator.ts b/apps/api/src/app/auth/roles.decorator.ts index c2e75ddf6..1ec5ac131 100644 --- a/apps/api/src/app/auth/roles.decorator.ts +++ b/apps/api/src/app/auth/roles.decorator.ts @@ -1,4 +1,5 @@ import { SetMetadata } from '@nestjs/common'; import { Role } from '@prisma/client'; -export const Roles = (...roles: Array) => SetMetadata('roles', roles); +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: Array) => SetMetadata(ROLES_KEY, roles);