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
- Usuario autenticado (JWT valido)
- Roles y permisos cargados en el sistema
- 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
// 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
// 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
// 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
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
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
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
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)
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
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
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
// 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
// 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<boolean> {
// 1. Verificar si es ruta publica
const isPublic = this.reflector.getAllAndOverride<boolean>(
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<string[]> {
const cacheKey = `permissions:${userId}`;
// Intentar desde cache
const cached = await this.cacheManager.get<string[]>(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
// guards/owner.guard.ts
@Injectable()
export class OwnerGuard implements CanActivate {
constructor(
private reflector: Reflector,
private permissionService: PermissionService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const ownerPermission = this.reflector.get<string>(
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
// 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
// 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
// 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 |