erp-core/orchestration/directivas/DIRECTIVA-MULTI-TENANT.md

5.4 KiB

Directiva: Multi-Tenant

Propósito

Define las reglas obligatorias para implementar multi-tenancy en ERP Core y todas las verticales que lo extienden.

Alcance

  • Backend: Todas las consultas y operaciones
  • Database: RLS Policies, schemas, índices
  • Frontend: Contexto de tenant
  • APIs: Headers y validación

Principios

1. Aislamiento Total

OBLIGATORIO: Ningún tenant puede ver o modificar datos de otro tenant.

2. Tenant ID Obligatorio

OBLIGATORIO: Toda tabla debe tener columna `tenant_id UUID NOT NULL`.

3. Filtrado Automático

OBLIGATORIO: Toda consulta debe filtrar por tenant_id a nivel de aplicación Y base de datos (RLS).

Implementación en Base de Datos

Columna tenant_id

-- OBLIGATORIO en TODA tabla
CREATE TABLE {schema}.{tabla} (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id),
    -- ... otras columnas ...
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Índice obligatorio
CREATE INDEX idx_{tabla}_tenant_id ON {schema}.{tabla}(tenant_id);

RLS Policy Estándar

-- Habilitar RLS
ALTER TABLE {schema}.{tabla} ENABLE ROW LEVEL SECURITY;

-- Policy de lectura
CREATE POLICY "tenant_isolation_select" ON {schema}.{tabla}
    FOR SELECT
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Policy de inserción
CREATE POLICY "tenant_isolation_insert" ON {schema}.{tabla}
    FOR INSERT
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Policy de actualización
CREATE POLICY "tenant_isolation_update" ON {schema}.{tabla}
    FOR UPDATE
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid)
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Policy de eliminación
CREATE POLICY "tenant_isolation_delete" ON {schema}.{tabla}
    FOR DELETE
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

Contexto de Sesión

-- Establecer contexto al inicio de cada request
SET app.current_tenant_id = '{tenant_uuid}';

-- En transacciones
BEGIN;
SET LOCAL app.current_tenant_id = '{tenant_uuid}';
-- ... operaciones ...
COMMIT;

Implementación en Backend

Middleware de Tenant

// middleware/tenant.middleware.ts
export const tenantMiddleware = async (req, res, next) => {
    const tenantId = req.headers['x-tenant-id'] || req.user?.tenantId;

    if (!tenantId) {
        return res.status(400).json({ error: 'Tenant ID required' });
    }

    // Validar que tenant existe y está activo
    const tenant = await tenantService.findById(tenantId);
    if (!tenant || !tenant.isActive) {
        return res.status(403).json({ error: 'Invalid or inactive tenant' });
    }

    req.tenantId = tenantId;

    // Establecer contexto en BD
    await dbConnection.query(`SET app.current_tenant_id = '${tenantId}'`);

    next();
};

En Servicios

// OBLIGATORIO: Siempre filtrar por tenantId
async findAll(tenantId: string): Promise<Entity[]> {
    return this.repository.find({
        where: { tenantId }
    });
}

// OBLIGATORIO: Validar tenantId en creación
async create(dto: CreateDto, tenantId: string): Promise<Entity> {
    const entity = this.repository.create({
        ...dto,
        tenantId
    });
    return this.repository.save(entity);
}

En Controllers

@Get()
async findAll(@Req() req: Request) {
    // tenantId viene del middleware
    return this.service.findAll(req.tenantId);
}

Implementación en Frontend

Contexto de Tenant

// stores/tenant.store.ts
interface TenantState {
    currentTenantId: string | null;
    tenants: Tenant[];
}

export const useTenantStore = create<TenantState>((set) => ({
    currentTenantId: null,
    tenants: [],
    setCurrentTenant: (id) => set({ currentTenantId: id }),
}));

En API Calls

// services/api.service.ts
const api = axios.create({
    baseURL: API_URL,
});

api.interceptors.request.use((config) => {
    const tenantId = useTenantStore.getState().currentTenantId;
    if (tenantId) {
        config.headers['X-Tenant-Id'] = tenantId;
    }
    return config;
});

Validaciones Obligatorias

Checklist Pre-Deploy

  • Toda tabla tiene columna tenant_id
  • Toda tabla tiene RLS habilitado
  • Toda tabla tiene policies de tenant
  • Todo endpoint usa middleware de tenant
  • Todo servicio filtra por tenant
  • Frontend envía header X-Tenant-Id

Tests Obligatorios

describe('Tenant Isolation', () => {
    it('should not return data from other tenants', async () => {
        // Crear datos en tenant A
        await service.create(data, tenantA);

        // Consultar desde tenant B
        const results = await service.findAll(tenantB);

        // No debe encontrar datos de tenant A
        expect(results).toHaveLength(0);
    });
});

Excepciones

Las siguientes tablas NO requieren tenant_id:

  • core_tenants.tenants (la tabla maestra de tenants)
  • core_auth.system_settings (configuración global del sistema)
  • core_catalogs.countries, core_catalogs.currencies (catálogos globales)

Referencias


Directiva específica de ERP-Core