erp-core/docs/01-fase-foundation/MGN-003-roles/requerimientos/RF-ROLE-004.md

13 KiB

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

// 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