# RF-ROLE-004: Guards y Middlewares RBAC ## Identificacion | Campo | Valor | |-------|-------| | **ID** | RF-ROLE-004 | | **Modulo** | MGN-003 Roles/RBAC | | **Prioridad** | P0 - Critica | | **Estado** | Ready | | **Fecha** | 2025-12-05 | --- ## Descripcion El sistema debe implementar mecanismos de control de acceso basado en roles (RBAC) que se apliquen automaticamente a todos los endpoints protegidos. Esto incluye guards de NestJS, decoradores personalizados y middlewares para validar permisos antes de ejecutar cualquier accion. --- ## Actores | Actor | Descripcion | |-------|-------------| | Sistema | Valida permisos en cada request | | Usuario | Sujeto de validacion de permisos | --- ## Precondiciones 1. Usuario autenticado (JWT valido) 2. Roles y permisos cargados en el sistema 3. Endpoint decorado con permisos requeridos --- ## Arquitectura RBAC ``` Request HTTP │ ▼ ┌─────────────────┐ │ JwtAuthGuard │ Valida token JWT └────────┬────────┘ │ ▼ ┌─────────────────┐ │ TenantGuard │ Verifica tenant del usuario └────────┬────────┘ │ ▼ ┌─────────────────┐ │ RbacGuard │ Valida permisos requeridos └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Controller │ Ejecuta logica de negocio └─────────────────┘ ``` --- ## Flujo de Validacion ### Validacion Estandar ``` 1. Request llega al servidor 2. JwtAuthGuard extrae y valida token 3. Sistema carga usuario y sus roles desde cache/DB 4. TenantGuard verifica que usuario pertenece al tenant 5. RbacGuard obtiene permisos requeridos del decorator 6. Sistema calcula permisos efectivos del usuario 7. Sistema verifica si tiene TODOS los permisos requeridos 8. Si tiene permisos: continua al controller 9. Si no tiene: retorna 403 Forbidden ``` ### Validacion con Permisos Alternativos (OR) ``` 1. Endpoint requiere: users:update OR users:admin 2. Usuario tiene: users:update 3. Sistema verifica si tiene AL MENOS UNO 4. Usuario tiene users:update -> acceso permitido ``` ### Validacion Condicional (Owner) ``` 1. Endpoint requiere: users:update OR ser owner del recurso 2. Sistema verifica permisos 3. Si no tiene permiso, verifica si es owner 4. Si es owner del recurso: acceso permitido ``` --- ## Componentes del Sistema ### 1. Decoradores ```typescript // Permiso requerido (AND) @Permissions('users:read', 'users:update') // Usuario debe tener AMBOS permisos // Permiso alternativo (OR) @AnyPermission('users:update', 'users:admin') // Usuario debe tener AL MENOS UNO // Rol requerido @Roles('admin', 'manager') // Usuario debe tener AL MENOS UNO de los roles // Acceso publico (sin auth) @Public() // Owner o permiso @OwnerOrPermission('users:update') ``` ### 2. Guards ```typescript // JwtAuthGuard - Ya implementado en MGN-001 // Valida token, extrae usuario // TenantGuard // Verifica tenant_id del usuario vs tenant del recurso // RbacGuard // Valida permisos segun decoradores ``` ### 3. Interceptors ```typescript // PermissionInterceptor // Agrega permisos efectivos al request para uso en controller // AuditInterceptor // Registra acciones sensibles con info de permisos ``` --- ## Reglas de Negocio | ID | Regla | |----|-------| | RN-001 | Permisos se validan en CADA request (no solo al login) | | RN-002 | Permisos efectivos se cachean por sesion (TTL 5 min) | | RN-003 | Cambios de roles invalidan cache de permisos | | RN-004 | Super Admin bypasea validacion de permisos | | RN-005 | Endpoints publicos no requieren autenticacion | | RN-006 | Error 403 no revela que permisos faltan (seguridad) | | RN-007 | Logs de acceso denegado para auditoria | --- ## Criterios de Aceptacion ### Escenario 1: Acceso con permiso valido ```gherkin Given un usuario con permiso "users:read" And endpoint GET /api/v1/users requiere "users:read" When el usuario hace la solicitud Then el sistema permite el acceso And retorna status 200 con los datos ``` ### Escenario 2: Acceso denegado por falta de permiso ```gherkin Given un usuario con permiso "users:read" solamente And endpoint DELETE /api/v1/users/:id requiere "users:delete" When el usuario intenta eliminar Then el sistema retorna status 403 And el mensaje es "No tienes permiso para realizar esta accion" And NO revela que permiso falta ``` ### Escenario 3: Wildcard permite acceso ```gherkin Given un usuario con permiso "users:*" And endpoint requiere "users:delete" When el usuario hace la solicitud Then el sistema permite el acceso And wildcard cubre el permiso especifico ``` ### Escenario 4: Super Admin bypass ```gherkin Given un usuario con rol "super_admin" And endpoint requiere "cualquier:permiso" When el usuario hace la solicitud Then el sistema permite el acceso And no valida permisos especificos ``` ### Escenario 5: Permisos alternativos (OR) ```gherkin Given endpoint requiere "users:update" OR "users:admin" And usuario tiene solo "users:admin" When el usuario hace la solicitud Then el sistema permite el acceso ``` ### Escenario 6: Owner puede acceder ```gherkin Given endpoint PATCH /api/v1/users/:id con @OwnerOrPermission('users:update') And usuario es owner del recurso (su propio perfil) And usuario NO tiene permiso "users:update" When el usuario actualiza su perfil Then el sistema permite el acceso And aplica validaciones de owner ``` ### Escenario 7: Cache de permisos ```gherkin Given permisos de usuario cacheados When el admin cambia roles del usuario Then el cache se invalida And siguiente request recalcula permisos ``` --- ## Notas Tecnicas ### Implementacion de Decoradores ```typescript // decorators/permissions.decorator.ts import { SetMetadata } from '@nestjs/common'; export const PERMISSIONS_KEY = 'permissions'; export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, { permissions, mode: 'AND' }); export const AnyPermission = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, { permissions, mode: 'OR' }); export const ROLES_KEY = 'roles'; export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); export const OWNER_OR_PERMISSION_KEY = 'ownerOrPermission'; export const OwnerOrPermission = (permission: string) => SetMetadata(OWNER_OR_PERMISSION_KEY, permission); ``` ### Implementacion de RbacGuard ```typescript // guards/rbac.guard.ts @Injectable() export class RbacGuard implements CanActivate { constructor( private reflector: Reflector, private permissionService: PermissionService, private cacheManager: Cache, ) {} async canActivate(context: ExecutionContext): Promise { // 1. Verificar si es ruta publica const isPublic = this.reflector.getAllAndOverride( IS_PUBLIC_KEY, [context.getHandler(), context.getClass()], ); if (isPublic) return true; // 2. Obtener usuario del request const request = context.switchToHttp().getRequest(); const user = request.user; if (!user) return false; // 3. Super Admin bypass if (user.roles.includes('super_admin')) return true; // 4. Obtener permisos requeridos const requiredPermissions = this.reflector.getAllAndOverride<{ permissions: string[]; mode: 'AND' | 'OR'; }>(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]); if (!requiredPermissions) return true; // No requiere permisos // 5. Obtener permisos efectivos (con cache) const effectivePermissions = await this.getEffectivePermissions(user.id); // 6. Validar permisos const { permissions, mode } = requiredPermissions; if (mode === 'AND') { return permissions.every(p => this.hasPermission(effectivePermissions, p)); } else { return permissions.some(p => this.hasPermission(effectivePermissions, p)); } } private hasPermission(userPerms: string[], required: string): boolean { // Verificar permiso directo if (userPerms.includes(required)) return true; // Verificar wildcard const [module] = required.split(':'); if (userPerms.includes(`${module}:*`)) return true; // Verificar wildcard de segundo nivel const parts = required.split(':'); if (parts.length === 3) { if (userPerms.includes(`${parts[0]}:${parts[1]}:*`)) return true; } return false; } private async getEffectivePermissions(userId: string): Promise { const cacheKey = `permissions:${userId}`; // Intentar desde cache const cached = await this.cacheManager.get(cacheKey); if (cached) return cached; // Calcular permisos const permissions = await this.permissionService.calculateEffective(userId); // Guardar en cache (5 minutos) await this.cacheManager.set(cacheKey, permissions, 300000); return permissions; } } ``` ### Implementacion de OwnerGuard ```typescript // guards/owner.guard.ts @Injectable() export class OwnerGuard implements CanActivate { constructor( private reflector: Reflector, private permissionService: PermissionService, ) {} async canActivate(context: ExecutionContext): Promise { const ownerPermission = this.reflector.get( OWNER_OR_PERMISSION_KEY, context.getHandler(), ); if (!ownerPermission) return true; const request = context.switchToHttp().getRequest(); const user = request.user; const resourceId = request.params.id; // Verificar si tiene permiso const hasPermission = await this.permissionService.userHas( user.id, ownerPermission, ); if (hasPermission) return true; // Verificar si es owner const isOwner = user.id === resourceId; return isOwner; } } ``` ### Uso en Controllers ```typescript // users.controller.ts @Controller('api/v1/users') @UseGuards(JwtAuthGuard, TenantGuard, RbacGuard) export class UsersController { @Get() @Permissions('users:read') findAll() { // Solo usuarios con users:read } @Post() @Permissions('users:create') create(@Body() dto: CreateUserDto) { // Solo usuarios con users:create } @Patch(':id') @OwnerOrPermission('users:update') update(@Param('id') id: string, @Body() dto: UpdateUserDto) { // Owner del recurso O usuarios con users:update } @Delete(':id') @Permissions('users:delete') remove(@Param('id') id: string) { // Solo usuarios con users:delete } @Get('export') @AnyPermission('users:export', 'users:admin') export() { // Usuarios con users:export O users:admin } } // public.controller.ts @Controller('api/v1/public') export class PublicController { @Get('health') @Public() health() { // Sin autenticacion } } ``` ### Invalidacion de Cache ```typescript // Al cambiar roles de usuario async updateUserRoles(userId: string, roleIds: string[]) { await this.userRoleRepository.update(userId, roleIds); // Invalidar cache de permisos await this.cacheManager.del(`permissions:${userId}`); // Emitir evento para invalidar en otros servicios this.eventEmitter.emit('user.roles.changed', { userId }); } // Al modificar permisos de un rol async updateRolePermissions(roleId: string, permissionIds: string[]) { await this.rolePermissionRepository.update(roleId, permissionIds); // Obtener usuarios con este rol const users = await this.userRoleRepository.findUsersByRole(roleId); // Invalidar cache de todos for (const user of users) { await this.cacheManager.del(`permissions:${user.id}`); } } ``` --- ## Respuestas de Error ```typescript // 401 Unauthorized - No autenticado { "statusCode": 401, "message": "No autenticado", "error": "Unauthorized" } // 403 Forbidden - Sin permiso { "statusCode": 403, "message": "No tienes permiso para realizar esta accion", "error": "Forbidden" } // NOTA: No revelar que permiso falta por seguridad // Log interno (no expuesto) { "userId": "user-uuid", "endpoint": "DELETE /api/v1/users/123", "requiredPermission": "users:delete", "userPermissions": ["users:read"], "result": "denied", "timestamp": "2025-12-05T10:00:00Z" } ``` --- ## Dependencias | ID | Descripcion | |----|-------------| | RF-AUTH-002 | JWT para autenticacion | | RF-ROLE-001 | Roles del sistema | | RF-ROLE-002 | Permisos del sistema | | RF-ROLE-003 | Asignacion roles-usuarios | | Redis | Cache de permisos | --- ## Estimacion | Tarea | Puntos | |-------|--------| | Backend: Decoradores | 2 | | Backend: RbacGuard | 3 | | Backend: OwnerGuard | 2 | | Backend: Cache de permisos | 2 | | Backend: Invalidacion cache | 2 | | Backend: Tests | 3 | | Documentacion | 1 | | **Total** | **15 SP** | --- ## Historial | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | System | Creacion inicial |