4.9 KiB
4.9 KiB
RF-MGN-001-004: Multi-Tenancy con Schema-Level Isolation
Módulo: MGN-001 - Fundamentos Prioridad: P0 (MVP) Story Points: 13 Estado: Definido Fecha: 2025-11-23
Descripción
El sistema debe soportar multi-tenancy con aislamiento a nivel de schema PostgreSQL. Cada tenant tiene su propio schema con sus datos completamente aislados. El sistema debe prevenir acceso cruzado entre tenants.
Actores
- Actor Principal: Sistema (automático)
- Actores Secundarios: Administrador de Plataforma (gestiona tenants)
Precondiciones
- Base de datos PostgreSQL debe estar disponible
- Schema "public" debe contener metadata de tenants
- Usuario de DB debe tener permisos para crear schemas
Flujo Principal
- Sistema recibe request HTTP con subdomain (ej: empresa1.erp.com)
- Middleware extrae subdomain del request
- Sistema consulta auth.tenants en schema "public" para obtener tenant_id
- Sistema valida que tenant.status = 'active'
- Sistema ejecuta
SET search_path TO tenant_{id}, public - Sistema establece contexto de tenant en request (req.tenant_id)
- Todas las queries subsecuentes se ejecutan en schema del tenant
- Sistema ejecuta lógica de negocio dentro del schema correcto
- Sistema resetea search_path al finalizar request
Flujos Alternativos
FA-1: Tenant No Encontrado
- Si subdomain no existe en auth.tenants
- Sistema retorna error 404: "Tenant no encontrado"
FA-2: Tenant Inactivo
- Si tenant.status != 'active'
- Sistema retorna error 403: "Tenant suspendido. Contacte al administrador"
FA-3: Creación de Nuevo Tenant
- Administrador de plataforma crea nuevo tenant
- Sistema genera tenant_id único
- Sistema crea schema:
CREATE SCHEMA tenant_{id} - Sistema ejecuta migrations en nuevo schema (todas las tablas)
- Sistema crea registro en auth.tenants (schema public)
- Sistema crea usuario administrador inicial en nuevo schema
- Tenant queda disponible para uso
FA-4: Intento de Acceso Cruzado
- Si código intenta acceder a datos de otro tenant
- Sistema valida tenant_id en middleware
- Sistema retorna error 403: "Acceso denegado"
- Sistema registra intento en audit_log
Reglas de Negocio
- RN-1: Cada tenant tiene su propio schema PostgreSQL (tenant_{id})
- RN-2: Schema "public" contiene solo metadata de tenants y configuración global
- RN-3: Usuarios, roles, permisos son por tenant (no globales)
- RN-4: Acceso cruzado entre tenants está PROHIBIDO
- RN-5: Función PostgreSQL
get_current_tenant_id()retorna tenant_id actual - RN-6: RLS (Row Level Security) adicional para seguridad en profundidad
- RN-7: Migrations deben ejecutarse en todos los schemas de tenants
Criterios de Aceptación
- Sistema identifica tenant por subdomain en cada request
- Cada tenant tiene su propio schema PostgreSQL aislado
- Queries se ejecutan automáticamente en schema correcto
- Tenant inactivo no permite acceso
- Tenant no encontrado retorna error 404
- Creación de tenant genera schema y tablas automáticamente
- Migrations se aplican a todos los schemas existentes
- No es posible acceder a datos de otro tenant
- Función
get_current_tenant_id()funciona correctamente - Audit log registra intentos de acceso cruzado
Entidades Involucradas
- Principales:
- auth.tenants (schema public: tenant_id, subdomain, status, created_at)
- tenant_{id}.* (todas las tablas dentro del schema del tenant)
- Relacionadas:
- information_schema.schemata (lista de schemas)
Referencias
Notas Técnicas
- Patrón Gamilit: Schema-level isolation para multi-tenancy
- PostgreSQL: Uso de
SET search_pathpara cambiar schema activo - Función SQL:
CREATE OR REPLACE FUNCTION get_current_tenant_id() RETURNS INTEGER AS $$ BEGIN RETURN current_setting('app.current_tenant_id')::INTEGER; END; $$ LANGUAGE plpgsql; - Middleware NestJS:
@Injectable() export class TenantMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction) { const subdomain = extractSubdomain(req.hostname); const tenant = await this.findTenant(subdomain); req.tenant = tenant; await this.setSearchPath(tenant.id); next(); } } - Migrations: Herramienta debe ejecutar migrations en todos los schemas tenant_*
- Performance: Conexión pool por tenant o shared pool con search_path dinámico
Dependencias
- RF Dependientes: Ninguno (es funcionalidad base)
- Bloqueante para: Todos los módulos (todos dependen de multi-tenancy)