erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-004-multi-tenancy.md

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

  1. Base de datos PostgreSQL debe estar disponible
  2. Schema "public" debe contener metadata de tenants
  3. Usuario de DB debe tener permisos para crear schemas

Flujo Principal

  1. Sistema recibe request HTTP con subdomain (ej: empresa1.erp.com)
  2. Middleware extrae subdomain del request
  3. Sistema consulta auth.tenants en schema "public" para obtener tenant_id
  4. Sistema valida que tenant.status = 'active'
  5. Sistema ejecuta SET search_path TO tenant_{id}, public
  6. Sistema establece contexto de tenant en request (req.tenant_id)
  7. Todas las queries subsecuentes se ejecutan en schema del tenant
  8. Sistema ejecuta lógica de negocio dentro del schema correcto
  9. Sistema resetea search_path al finalizar request

Flujos Alternativos

FA-1: Tenant No Encontrado

  1. Si subdomain no existe en auth.tenants
  2. Sistema retorna error 404: "Tenant no encontrado"

FA-2: Tenant Inactivo

  1. Si tenant.status != 'active'
  2. Sistema retorna error 403: "Tenant suspendido. Contacte al administrador"

FA-3: Creación de Nuevo Tenant

  1. Administrador de plataforma crea nuevo tenant
  2. Sistema genera tenant_id único
  3. Sistema crea schema: CREATE SCHEMA tenant_{id}
  4. Sistema ejecuta migrations en nuevo schema (todas las tablas)
  5. Sistema crea registro en auth.tenants (schema public)
  6. Sistema crea usuario administrador inicial en nuevo schema
  7. Tenant queda disponible para uso

FA-4: Intento de Acceso Cruzado

  1. Si código intenta acceder a datos de otro tenant
  2. Sistema valida tenant_id en middleware
  3. Sistema retorna error 403: "Acceso denegado"
  4. 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_path para 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)