From 317fdfd8c2e7f50900fa88282df79348b0663da5 Mon Sep 17 00:00:00 2001 From: Fares Eidi Date: Mon, 27 Nov 2023 13:03:37 +0100 Subject: [PATCH] fix pr review comments - adding line breaks for style rules --- .../decorators/has-permission.decorator.ts | 3 + .../src/guards/has-permissions.guard.spec.ts | 57 +++++++++++++++++++ apps/api/src/guards/has-permissions.guard.ts | 30 ++++++++++ 3 files changed, 90 insertions(+) create mode 100644 apps/api/src/decorators/has-permission.decorator.ts create mode 100644 apps/api/src/guards/has-permissions.guard.spec.ts create mode 100644 apps/api/src/guards/has-permissions.guard.ts diff --git a/apps/api/src/decorators/has-permission.decorator.ts b/apps/api/src/decorators/has-permission.decorator.ts new file mode 100644 index 000000000..cd350edc6 --- /dev/null +++ b/apps/api/src/decorators/has-permission.decorator.ts @@ -0,0 +1,3 @@ +import { SetMetadata } from '@nestjs/common'; +export const HAS_PERMISSION_KEY = 'has_permission'; +export const HasPermission = (permission: string) => SetMetadata(HAS_PERMISSION_KEY, permission); diff --git a/apps/api/src/guards/has-permissions.guard.spec.ts b/apps/api/src/guards/has-permissions.guard.spec.ts new file mode 100644 index 000000000..18c694f35 --- /dev/null +++ b/apps/api/src/guards/has-permissions.guard.spec.ts @@ -0,0 +1,57 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HasPermissionsGuard } from './has-permissions.guard'; +import { Reflector } from '@nestjs/core'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { HttpException } from '@nestjs/common'; + +describe('HasPermissionsGuard', () => { + let guard: HasPermissionsGuard; + let reflector: Reflector; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + HasPermissionsGuard, + Reflector, + ], + }).compile(); + + guard = module.get(HasPermissionsGuard); + reflector = module.get(Reflector); + }); + + function setupReflectorSpy(returnValue: string) { + jest.spyOn(reflector, 'get').mockReturnValue(returnValue); + } + + function createMockExecutionContext(permissions: string[]) { + return new ExecutionContextHost([ + { + user: { + permissions, // Set user permissions based on the argument + }, + }, + ]); + } + + it('should deny access if the user does not have any permission', () => { + setupReflectorSpy('required-permission'); + const noPermissions = createMockExecutionContext([]); + + expect(() => guard.canActivate(noPermissions)).toThrow(HttpException); + }); + + it('should deny access if the user has the wrong permission', () => { + setupReflectorSpy('required-permission'); + const wrongPermission = createMockExecutionContext(['wrong-permission']); + + expect(() => guard.canActivate(wrongPermission)).toThrow(HttpException); + }); + + it('should allow access if the user has the required permission', () => { + setupReflectorSpy('required-permission'); + const rightPermission = createMockExecutionContext(['required-permission']); + + expect(guard.canActivate(rightPermission)).toBe(true); + }); +}); diff --git a/apps/api/src/guards/has-permissions.guard.ts b/apps/api/src/guards/has-permissions.guard.ts new file mode 100644 index 000000000..545bed0c7 --- /dev/null +++ b/apps/api/src/guards/has-permissions.guard.ts @@ -0,0 +1,30 @@ +import { Injectable, CanActivate, ExecutionContext, HttpException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { HAS_PERMISSION_KEY } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { hasPermission } from '@ghostfolio/common/permissions'; + +@Injectable() +export class HasPermissionsGuard implements CanActivate { + constructor(private reflector: Reflector) { } + + canActivate(context: ExecutionContext): boolean { + const requiredPermission = this.reflector.get(HAS_PERMISSION_KEY, context.getHandler()); + + if (!requiredPermission) { + return true; // No specific permissions required + } + + const request = context.switchToHttp().getRequest(); + const user = request.user; + + if (!user || !hasPermission(user.permissions, requiredPermission)) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return true; + } +}