5.4 KiB
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
- RLS PostgreSQL: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- Directiva Diseño BD:
DIRECTIVA-DISENO-BASE-DATOS.md
Directiva específica de ERP-Core