# Módulo de Permisos Multi-Tenant

Sistema completo de permisos granulares para aplicaciones multi-tenant con FastAPI y SQLAlchemy.

## 📁 Estructura del Módulo

```
backend/api/v1/modules/core/permissions/
├── __init__.py          # Exports del módulo
├── models.py            # Modelos SQLAlchemy
├── service.py           # Lógica de negocio
├── dependencies.py      # Dependencias FastAPI
├── schemas.py           # Modelos Pydantic (request/response)
└── routes.py            # Endpoints de la API
```

## 🎯 Componentes

### **models.py**

Define los modelos de base de datos:

- `Permission` - Permisos del sistema (ej: "invoice.view", "invoice.edit")
- `ClientRole` - Roles personalizados por cliente
- `RolePermission` - Relación roles-permisos
- `UserClientRole` - Asignación usuario-rol-cliente
- `UserClientPermission` - Permisos directos por usuario

### **service.py**

Contiene la clase `PermissionService` con métodos:

- `get_user_permissions()` - Obtiene todos los permisos de un usuario
- `has_permission()` - Verifica un permiso específico
- `has_all_permissions()` - Verifica múltiples permisos (AND)
- `has_any_permission()` - Verifica múltiples permisos (OR)
- `assign_role_to_user()` - Asigna roles a usuarios
- `grant_direct_permission()` - Concede permisos directos

### **dependencies.py**

Dependencias para proteger rutas:

- `PermissionChecker` - Clase para verificar múltiples permisos
- `RequirePermission` - Clase para verificar un solo permiso
- `get_client_id()` - Extrae el ID del cliente del header
- `get_permission_service()` - Proporciona instancia del servicio
- `get_current_user_permissions()` - Devuelve permisos del usuario

### **schemas.py**

Modelos Pydantic para request/response:

- Responses: `PermissionResponse`, `ClientRoleResponse`, `UserPermissionsResponse`, etc.
- Requests: `AssignRoleRequest`, `GrantPermissionRequest`, `CreateRoleRequest`, etc.

### **routes.py**

Endpoints de la API:

- `GET /permissions/me` - Permisos del usuario actual
- `GET /permissions/available` - Lista todos los permisos
- `GET /permissions/roles` - Lista roles del cliente
- `POST /permissions/roles` - Crea un rol
- `POST /permissions/assign-role` - Asigna rol a usuario
- `POST /permissions/grant-permission` - Concede permiso directo
- Ejemplos de rutas protegidas

## 🚀 Uso Rápido

### Importar el módulo

```python
from api.v1.modules.core.permissions import (
    Permission,
    ClientRole,
    PermissionService,
    PermissionChecker,
    RequirePermission,
    router
)
```

### Registrar las rutas

```python
# En backend/api/v1/router.py
from api.v1.modules.core.permissions import router as permissions_router

api_router = APIRouter()
api_router.include_router(permissions_router)
```

### Proteger una ruta con permiso único

```python
from fastapi import APIRouter, Depends
from api.v1.modules.core.permissions import RequirePermission

router = APIRouter()

@router.get("/invoices")
async def list_invoices(
    _: None = Depends(RequirePermission("invoice.view"))
):
    return {"invoices": [...]}
```

### Proteger con múltiples permisos

```python
from api.v1.modules.core.permissions import PermissionChecker

@router.post("/invoices")
async def create_invoice(
    _: None = Depends(PermissionChecker(
        ["invoice.view", "invoice.create"],
        require_all=True  # Requiere TODOS
    ))
):
    return {"created": True}
```

### Usar permisos en la lógica

```python
from api.v1.modules.core.permissions import get_current_user_permissions

@router.get("/dashboard")
async def dashboard(
    permissions: set = Depends(get_current_user_permissions)
):
    widgets = []
  
    if "invoice.view" in permissions:
        widgets.append({"type": "invoices", "data": [...]})
  
    return {"widgets": widgets}
```

## 📊 Base de Datos

### Ejecutar migración

```bash
cd backend
alembic upgrade head
```

Esto crea las tablas y permisos iniciales:

- **invoice.*** - view, create, edit, delete, approve
- **user.*** - view, create, edit, delete
- **report.*** - financial.view, admin.view, export
- **roles.*** - view, create, edit, delete, assign
- **permissions.*** - view, grant

## 🔐 Flujo de Autenticación

1. Usuario hace request con token JWT de Keycloak
2. Header `X-Client-ID` indica el cliente/tenant
3. Sistema extrae `user_id` del token
4. Consulta permisos del usuario en ese cliente
5. Valida si tiene el permiso requerido
6. Devuelve 200 OK o 403 Forbidden

## 💡 Ejemplos Prácticos

### Crear un rol personalizado

```python
from api.v1.modules.core.permissions import PermissionService
from core.database import get_db

db = next(get_db())
service = PermissionService(db)

# Crear rol
role = ClientRole(
    client_id=1,
    name="Contador",
    code="accountant",
    description="Acceso a módulo contable"
)
db.add(role)
db.commit()
```

### Asignar permisos a un rol

```python
from api.v1.modules.core.permissions.models import RolePermission

# Obtener permisos de facturación
invoice_perms = db.query(Permission).filter(
    Permission.module == "invoice"
).all()

# Asignar al rol
for perm in invoice_perms:
    role_perm = RolePermission(
        client_role_id=role.id,
        permission_id=perm.id
    )
    db.add(role_perm)

db.commit()
```

### Asignar rol a usuario

```python
service.assign_role_to_user(
    user_id="user-uuid-from-keycloak",
    client_id=1,
    role_id=role.id,
    assigned_by="admin-uuid"
)
```

### Conceder permiso temporal

```python
from datetime import datetime, timedelta

service.grant_direct_permission(
    user_id="user-uuid",
    client_id=1,
    permission_code="invoice.delete",
    assigned_by="admin-uuid",
    expires_at=datetime.utcnow() + timedelta(days=7)
)
```

## ⚡ Optimización de Rendimiento

### 1. Caché con Redis

```python
import redis
from functools import lru_cache

redis_client = redis.Redis(host='localhost', port=6379)

def get_cached_permissions(user_id: str, client_id: int) -> set:
    cache_key = f"perms:{user_id}:{client_id}"
  
    cached = redis_client.get(cache_key)
    if cached:
        return set(cached.decode().split(','))
  
    # Consultar DB
    service = PermissionService(db)
    permissions = service.get_user_permissions(user_id, client_id)
  
    # Cachear por 5 minutos
    redis_client.setex(cache_key, 300, ','.join(permissions))
  
    return permissions
```

### 2. Índices de Base de Datos

Ya están definidos en los modelos:

- Índices compuestos para consultas eficientes
- Índices únicos para prevenir duplicados
- Índices en foreign keys

### 3. Query Optimization

El servicio usa JOINs eficientes en lugar de N+1 queries.

## 🧪 Testing

```python
import pytest
from api.v1.modules.core.permissions import PermissionService
from api.v1.modules.core.permissions.models import Permission, ClientRole

def test_user_has_permission_from_role(db_session):
    # Setup
    perm = Permission(code="invoice.view", module="invoice", action="view")
    db_session.add(perm)
  
    role = ClientRole(client_id=1, code="viewer", name="Viewer")
    db_session.add(role)
    db_session.commit()
  
    # Test
    service = PermissionService(db_session)
    assert service.has_permission("user-123", 1, "invoice.view")
```

## 📝 Notas Importantes

- **Client ID**: Por defecto se obtiene del header `X-Client-ID`, pero puede adaptarse a subdominios o JWT
- **User ID**: Se extrae del campo `sub` del token JWT de Keycloak
- **Permisos Directos**: Pueden revocar permisos heredados de roles (`is_granted=False`)
- **Soft Delete**: Los roles y permisos se desactivan (`is_active=False`) en lugar de eliminarse

## 🔗 Integración con Keycloak

Los roles globales de Keycloak pueden coexistir con los roles locales:

```python
@router.get("/protected")
async def protected_route(
    current_user: dict = Depends(get_current_user),
    permissions: set = Depends(get_current_user_permissions)
):
    # Verificar rol global de Keycloak
    keycloak_roles = current_user.get("realm_access", {}).get("roles", [])
  
    if "super_admin" in keycloak_roles:
        # Super admin tiene acceso total
        return {"access": "granted", "level": "global"}
  
    # Verificar permisos a nivel de cliente
    if "invoice.view" in permissions:
        return {"access": "granted", "level": "client"}
  
    raise HTTPException(403, "No access")
```
