531 lines
13 KiB
Markdown
531 lines
13 KiB
Markdown
# 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<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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```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 |
|