fix(server): premissions guard for read and write endpoints

This commit is contained in:
Ahmed Bouhuolia
2026-02-15 22:55:10 +02:00
parent af80afcf59
commit 2d39e38578
31 changed files with 423 additions and 21 deletions

View File

@@ -0,0 +1,67 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { REQUIRED_PERMISSION_KEY, RequiredPermission } from './RequirePermission.decorator';
import { AbilitySubject } from './Roles.types';
/**
* Guard that checks if the user has the required permission to access a route.
* Uses CASL ability instance attached to the request by AuthorizationGuard.
*/
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
/**
* Checks if the user has the required permission to access the route.
* @param context - The execution context
* @returns A boolean indicating if the user can access the route
* @throws ForbiddenException if the user doesn't have the required permission
*/
canActivate(context: ExecutionContext): boolean {
const requiredPermission = this.reflector.getAllAndOverride<RequiredPermission>(
REQUIRED_PERMISSION_KEY,
[context.getHandler(), context.getClass()],
);
// If no permission is required, allow access
if (!requiredPermission) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const ability = (request as any).ability;
// If no ability instance is attached to the request, deny access
if (!ability) {
throw new ForbiddenException('Ability instance not found. Ensure AuthorizationGuard is applied.');
}
const { ability: action, subject } = requiredPermission;
// Check if the user has the required permission using CASL ability
const hasPermission = ability.can(action, subject);
if (!hasPermission) {
throw new ForbiddenException(
`You do not have permission to ${action} ${subject}`,
);
}
return true;
}
/**
* Helper method to check if a subject value is a valid AbilitySubject.
* @param subject - The subject value to check
* @returns True if the subject is a valid AbilitySubject enum value
*/
private isValidSubject(subject: string): subject is AbilitySubject {
return Object.values(AbilitySubject).includes(subject as AbilitySubject);
}
}

View File

@@ -0,0 +1,29 @@
import { SetMetadata } from '@nestjs/common';
import { AbilitySubject } from './Roles.types';
export const REQUIRED_PERMISSION_KEY = 'requiredPermission';
export interface RequiredPermission {
ability: string;
subject: AbilitySubject | string;
}
/**
* Decorator to specify required ability and subject for a route handler or controller.
* @param ability - The ability/action required (e.g., 'Create', 'View', 'Edit', 'Delete')
* @param subject - The subject/entity the ability applies to (e.g., AbilitySubject.Item, AbilitySubject.SaleInvoice)
* @example
* ```typescript
* @RequirePermission('Create', AbilitySubject.Item)
* @Post()
* async createItem(@Body() dto: CreateItemDto) { ... }
*
* @RequirePermission('View', AbilitySubject.SaleInvoice)
* @Get(':id')
* async getInvoice(@Param('id') id: number) { ... }
* ```
*/
export const RequirePermission = (
ability: string,
subject: AbilitySubject | string,
) => SetMetadata(REQUIRED_PERMISSION_KEY, { ability, subject });

View File

@@ -10,6 +10,8 @@ import { RolePermission } from './models/RolePermission.model';
import { RolesController } from './Roles.controller';
import { RolesApplication } from './Roles.application';
import { RolePermissionsSchema } from './queries/RolePermissionsSchema';
import { AuthorizationGuard } from './Authorization.guard';
import { PermissionGuard } from './Permission.guard';
const models = [
RegisterTenancyModel(Role),
@@ -25,9 +27,11 @@ const models = [
GetRoleService,
GetRolesService,
RolesApplication,
RolePermissionsSchema
RolePermissionsSchema,
AuthorizationGuard,
PermissionGuard,
],
controllers: [RolesController],
exports: [...models],
exports: [...models, AuthorizationGuard, PermissionGuard],
})
export class RolesModule {}

View File

@@ -1,3 +1,4 @@
import { IsOptional } from '@/common/decorators/Validators';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
@@ -41,7 +42,7 @@ export class CommandRolePermissionDto {
export class CreateRolePermissionDto extends CommandRolePermissionDto { }
export class EditRolePermissionDto extends CommandRolePermissionDto {
@IsNumber()
@IsNotEmpty()
@IsOptional()
@ApiProperty({
example: 1,
description: 'The permission ID',
@@ -59,7 +60,6 @@ class CommandRoleDto {
roleName: string;
@IsString()
@IsNotEmpty()
@ApiProperty({
example: 'Administrator',
description: 'The description of the role',
@@ -71,9 +71,9 @@ export class CreateRoleDto extends CommandRoleDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CommandRolePermissionDto)
@Type(() => CreateRolePermissionDto)
@ApiProperty({
type: [CommandRolePermissionDto],
type: [CreateRolePermissionDto],
description: 'The permissions of the role',
})
permissions: Array<CreateRolePermissionDto>;