# US-MGN-001-002-003: Validar Permisos en Runtime (Guards/Middleware)

**RF Asociado:** [RF-MGN-001-002](../../02-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-002-gestion-roles.md)
**Módulo:** MGN-001 - Fundamentos
**Epic:** Autenticación y Seguridad
**Prioridad:** P0 (MVP)
**Story Points:** 3
**Sprint:** Sprint 2
**Estado:** Ready for Development
**Fecha:** 2025-11-24

---

## User Story

**Como** sistema de seguridad,
**Quiero** validar automáticamente los permisos del usuario antes de ejecutar cualquier acción,
**Para** garantizar que solo usuarios autorizados puedan acceder a funcionalidades específicas.

---

## Descripción Detallada

Esta user story implementa el middleware/guard que valida permisos en cada request. El sistema debe:
1. Interceptar requests a endpoints protegidos
2. Extraer user_id y tenant_id del JWT token
3. Consultar permisos del usuario (roles + permisos, con caché)
4. Validar si tiene el permiso requerido (ej: 'sale.order.create')
5. Si tiene permiso: continuar con la acción
6. Si NO tiene permiso: retornar 403 Forbidden y registrar en audit_log

---

## Criterios de Aceptación

### Escenario 1: Usuario con permiso ejecuta acción
**Dado que** soy usuario con rol "Contador" que tiene permiso 'account.invoice.create',
**Cuando** intento crear una factura via POST /api/v1/invoices,
**Entonces** el middleware valida mis permisos, confirma que tengo el permiso, y permite la ejecución.

### Escenario 2: Usuario sin permiso intenta acción
**Dado que** soy usuario con rol "Auditor" que solo tiene 'account.invoice.read',
**Cuando** intento eliminar una factura via DELETE /api/v1/invoices/123,
**Entonces** el middleware retorna 403 "No tiene permisos para esta acción" y registra el intento en audit_log.

### Escenario 3: Administrador bypass RBAC
**Dado que** soy usuario con rol "Administrator" (is_admin=true),
**Cuando** intento cualquier acción,
**Entonces** el middleware hace bypass de validación de permisos y permite todo.

### Escenario 4: Usuario con múltiples roles (allow overrides deny)
**Dado que** tengo rol "Auditor" (solo read) y rol "Contador" (CRUD),
**Cuando** intento crear factura,
**Entonces** el sistema aplica política "allow overrides deny" y permite la acción (porque Contador lo permite).

### Escenario 5: Permisos cacheados actualizados
**Dado que** administrador actualiza mis permisos (agrega 'sale.order.create'),
**Cuando** intento crear orden de venta,
**Entonces** el sistema detecta caché inválido, recarga permisos, y permite la acción.

### Escenario 6: Frontend condiciona UI según permisos
**Dado que** no tengo permiso 'account.payment.delete',
**Cuando** el frontend consulta mis permisos via hook usePermissions(),
**Entonces** el botón "Eliminar Pago" no se renderiza en la UI.

---

## Reglas de Negocio

- **RN-1:** Todos los endpoints transaccionales deben estar protegidos con guard de permisos.
- **RN-2:** Rol Administrator (is_admin=true) hace bypass completo de RBAC.
- **RN-3:** Usuario con múltiples roles: "allow overrides deny".
- **RN-4:** Permisos se cachean en Redis con TTL 1 hora.
- **RN-5:** Al cambiar permisos de rol, se invalida caché de todos los usuarios con ese rol.
- **RN-6:** Intentos de acceso no autorizado se registran en audit_log para auditoría.
- **RN-7:** Performance: validación de permisos < 10ms (usando caché).

---

## Tareas Técnicas (Checklist de Implementación)

### Backend
- [ ] Crear guard: `PermissionsGuard` que implementa `CanActivate`
- [ ] Crear decorador: `@RequirePermission('model.action')` para endpoints
- [ ] Implementar método: `PermissionsService.getUserPermissions(userId)` con caché
- [ ] Implementar método: `PermissionsService.hasPermission(userId, permission)`
- [ ] Implementar lógica de "allow overrides deny" para múltiples roles
- [ ] Implementar bypass para is_admin=true
- [ ] Implementar caché de permisos en Redis (key: `permissions:user:{userId}`)
- [ ] Implementar invalidación de caché al actualizar permisos de rol
- [ ] Crear endpoint: `GET /api/v1/auth/me/permissions` - Listar permisos del usuario actual
- [ ] Registrar intentos no autorizados en audit_log (user_id, action, timestamp, ip)
- [ ] Agregar unit tests (10 test cases)
- [ ] Agregar integration tests (8 test cases)
- [ ] Documentar en Swagger respuestas 403

### Frontend
- [ ] Crear hook: `usePermissions(permissions: string[])` que retorna boolean[]
- [ ] Crear hook: `useHasPermission(permission: string)` que retorna boolean
- [ ] Crear componente: `<PermissionGuard permission="sale.order.create"><Button/></PermissionGuard>`
- [ ] Crear API client: `authApi.getMyPermissions()`
- [ ] Implementar Zustand store: `useAuthStore.permissions`
- [ ] Cargar permisos del usuario en login y almacenar en store
- [ ] Implementar condicional rendering basado en permisos
- [ ] Agregar component tests (6 test cases)
- [ ] Documentar uso de hooks en README

### Database
- [ ] Verificar tablas necesarias (roles, permissions, user_roles)
- [ ] Crear tabla: `audit.access_log` (user_id, action, resource, success, ip, timestamp)
- [ ] Crear índice: `idx_access_log_user_timestamp` para queries de auditoría
- [ ] Crear vista materializada (opcional): `user_permissions_view` para performance

---

## Mockups / Wireframes

**No hay UI específica (es funcionalidad de backend/middleware).**

**Ejemplo de UI condicionado por permisos:**
```tsx
const { hasPermission } = usePermissions(['sale.order.create', 'sale.order.delete']);

return (
  <>
    {hasPermission('sale.order.create') && <Button>Nueva Orden</Button>}
    {hasPermission('sale.order.delete') && <Button>Eliminar</Button>}
  </>
);
```

---

## Casos de Prueba (Test Scenarios)

### Pruebas Funcionales
1. **TC-001: Permiso concedido**
   - **Input:** usuario con 'sale.order.create' intenta POST /orders
   - **Expected:** Request pasa guard, acción ejecutada

2. **TC-002: Permiso denegado**
   - **Input:** usuario sin 'sale.order.delete' intenta DELETE /orders/1
   - **Expected:** 403 Forbidden, audit_log registrado

3. **TC-003: Administrator bypass**
   - **Input:** usuario Administrator intenta cualquier acción
   - **Expected:** Bypass validación, siempre permitido

4. **TC-004: Múltiples roles (allow overrides deny)**
   - **Input:** usuario con RolA (deny) y RolB (allow)
   - **Expected:** Acción permitida (allow wins)

5. **TC-005: Caché hit**
   - **Input:** 2 requests seguidas del mismo usuario
   - **Expected:** 1ra consulta DB, 2da usa caché Redis

6. **TC-006: Caché invalidation**
   - **Input:** actualizar permisos de rol, usuario intenta acción
   - **Expected:** Caché invalidado, consulta DB, permisos actualizados

7. **TC-007: Frontend condicional rendering**
   - **Input:** usePermissions(['sale.order.create'])
   - **Expected:** Hook retorna [true] si tiene permiso, [false] si no

### Pruebas No Funcionales
1. **Performance:**
   - Validación con caché: < 10ms
   - Validación sin caché (cache miss): < 50ms
2. **Seguridad:**
   - No exponer información sensible en mensajes de error
   - Registrar TODOS los intentos de acceso no autorizado
3. **Concurrencia:**
   - Manejar race conditions en invalidación de caché

---

## Dependencias

- **User Stories bloqueantes:**
  - US-MGN-001-002-001 (Crear Roles)
  - US-MGN-001-002-002 (Asignar Permisos)
- **Módulos requeridos:** Redis para caché
- **Datos maestros necesarios:** Roles y permisos configurados

---

## Notas de Implementación

**Backend - NestJS Guard:**
```typescript
@Injectable()
export class PermissionsGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const user = request.user; // Del JWT
    const requiredPermission = this.reflector.get('permission', context.getHandler());

    if (user.is_admin) return true; // Bypass

    const hasPermission = await this.permissionsService.hasPermission(
      user.id,
      requiredPermission
    );

    if (!hasPermission) {
      await this.auditLog(user.id, requiredPermission, false);
      throw new ForbiddenException('No tiene permisos para esta acción');
    }

    return true;
  }
}
```

**Frontend - React Hook:**
```typescript
export function usePermissions(permissions: string[]) {
  const { user } = useAuthStore();

  return permissions.map(perm =>
    user.permissions?.includes(perm) || user.is_admin
  );
}
```

---

## Estimación Detallada

| Tarea | Estimación |
|-------|------------|
| Backend Development | 2 horas |
| Frontend Development | 2 horas |
| Testing (Unit + Integration) | 1.5 horas |
| Code Review | 0.5 horas |
| **TOTAL** | **6 horas** |

**Equivalente:** 3 Story Points

---

## Definition of Done (DoD)

- [ ] PermissionsGuard implementado y funcionando
- [ ] Decorador @RequirePermission funcionando
- [ ] Caché de permisos en Redis implementado
- [ ] Hooks usePermissions y useHasPermission funcionando
- [ ] Audit log de accesos no autorizados funcionando
- [ ] Unit tests escritos y pasando (cobertura > 80%)
- [ ] Integration tests escritos y pasando
- [ ] Code review completado
- [ ] Documentación actualizada
- [ ] Merge a branch `develop` completado

---

## Referencias

- [RF-MGN-001-002: Gestión de Roles y Permisos](../../02-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-002-gestion-roles.md)
- [ET-BACKEND-MGN-001-002](../../02-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-002-gestion-roles.md)
- [ET-FRONTEND-MGN-001-002](../../02-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-002-gestion-roles.md)
- [TRACEABILITY-MGN-001.yaml](../../02-modelado/trazabilidad/TRACEABILITY-MGN-001.yaml)
