RF-TENANT-003: Aislamiento de Datos
Identificacion
| Campo |
Valor |
| ID |
RF-TENANT-003 |
| Modulo |
MGN-004 Tenants |
| Prioridad |
P0 - Critica |
| Estado |
Ready |
| Fecha |
2025-12-05 |
Descripcion
El sistema debe garantizar el aislamiento completo de datos entre tenants. Ningun usuario de un tenant debe poder acceder, ver o modificar datos de otro tenant. Este aislamiento se implementa mediante Row Level Security (RLS) en PostgreSQL, con validacion adicional en la capa de aplicacion.
Actores
| Actor |
Descripcion |
| Sistema |
Aplica automaticamente el filtro de tenant |
| Usuario |
Cualquier usuario autenticado |
| Platform Admin |
Puede acceder a multiples tenants (con switch explicito) |
Estrategia de Aislamiento
Arquitectura: Shared Database + RLS
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL Database │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ Tenant A │ │ Tenant B │ │ Tenant C │ │
│ │ (tenant_id=1) │ │ (tenant_id=2) │ │ (tenant_id=3)│ │
│ │ │ │ │ │ │ │
│ │ users: 50 │ │ users: 30 │ │ users: 100 │ │
│ │ products: 1000 │ │ products: 500 │ │ products: 2k │ │
│ │ orders: 5000 │ │ orders: 2000 │ │ orders: 10k │ │
│ └──────────────────┘ └──────────────────┘ └───────────────┘ │
│ │
│ RLS Policies: Todas las tablas filtran por tenant_id │
│ app.current_tenant_id = variable de sesion │
│ │
└─────────────────────────────────────────────────────────────────┘
Capas de Proteccion
Request HTTP
│
▼
┌─────────────────┐
│ 1. JwtAuthGuard │ Extrae tenant_id del token
└────────┬────────┘
│
▼
┌─────────────────┐
│ 2. TenantGuard │ Valida tenant activo, setea contexto
└────────┬────────┘
│
▼
┌─────────────────┐
│ 3. Middleware │ SET app.current_tenant_id = :tenantId
│ PostgreSQL │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 4. RLS Policy │ WHERE tenant_id = current_setting('app.current_tenant_id')
│ PostgreSQL │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 5. Application │ Validacion adicional en queries
│ Layer │
└─────────────────┘
Implementacion RLS
Configuracion Base
-- Habilitar RLS en todas las tablas con tenant_id
ALTER TABLE core_users.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE core_rbac.roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE core_inventory.products ENABLE ROW LEVEL SECURITY;
-- ... todas las tablas de negocio
-- Funcion para obtener tenant actual
CREATE OR REPLACE FUNCTION current_tenant_id()
RETURNS UUID AS $$
BEGIN
RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID;
END;
$$ LANGUAGE plpgsql STABLE;
Politicas de Lectura
-- Usuarios solo ven datos de su tenant
CREATE POLICY tenant_isolation_select ON core_users.users
FOR SELECT
USING (tenant_id = current_tenant_id());
-- Platform Admin puede ver todos (con contexto especial)
CREATE POLICY platform_admin_select ON core_users.users
FOR SELECT
USING (
tenant_id = current_tenant_id()
OR current_setting('app.is_platform_admin', true) = 'true'
);
Politicas de Escritura
-- Insert: debe ser del tenant actual
CREATE POLICY tenant_isolation_insert ON core_users.users
FOR INSERT
WITH CHECK (tenant_id = current_tenant_id());
-- Update: solo registros del tenant actual
CREATE POLICY tenant_isolation_update ON core_users.users
FOR UPDATE
USING (tenant_id = current_tenant_id())
WITH CHECK (tenant_id = current_tenant_id());
-- Delete: solo registros del tenant actual
CREATE POLICY tenant_isolation_delete ON core_users.users
FOR DELETE
USING (tenant_id = current_tenant_id());
Reglas de Negocio
| ID |
Regla |
| RN-001 |
Toda tabla de datos de negocio DEBE tener columna tenant_id |
| RN-002 |
tenant_id es NOT NULL y tiene FK a tenants |
| RN-003 |
RLS habilitado en TODAS las tablas con tenant_id |
| RN-004 |
Queries sin contexto de tenant fallan (no retornan datos) |
| RN-005 |
Platform Admin debe hacer switch explicito de tenant |
| RN-006 |
Logs de auditoria registran tenant_id |
| RN-007 |
Backups se pueden hacer por tenant individual |
| RN-008 |
Indices deben incluir tenant_id para performance |
Criterios de Aceptacion
Escenario 1: Usuario solo ve datos de su tenant
Given usuario de Tenant A autenticado
And 100 productos en Tenant A
And 50 productos en Tenant B
When consulta GET /api/v1/products
Then solo ve los 100 productos de Tenant A
And no ve ningun producto de Tenant B
Escenario 2: Usuario no puede acceder a recurso de otro tenant
Given usuario de Tenant A autenticado
And producto "P-001" pertenece a Tenant B
When intenta GET /api/v1/products/P-001
Then el sistema responde con status 404
And el mensaje es "Recurso no encontrado"
And NO revela que el recurso existe en otro tenant
Escenario 3: Crear recurso asigna tenant automaticamente
Given usuario de Tenant A autenticado
When crea un producto sin especificar tenant_id
Then el sistema asigna automaticamente tenant_id de Tenant A
And el producto queda en Tenant A
Escenario 4: No permitir modificar tenant_id
Given usuario de Tenant A autenticado
And producto existente en Tenant A
When intenta actualizar tenant_id a Tenant B
Then el sistema responde con status 400
And el mensaje es "No se puede cambiar el tenant de un recurso"
Escenario 5: Platform Admin switch de tenant
Given Platform Admin autenticado
When hace POST /api/v1/platform/switch-tenant/tenant-b-id
Then el contexto cambia a Tenant B
And puede ver datos de Tenant B
And no ve datos de Tenant A
Escenario 6: Query sin contexto de tenant
Given conexion a base de datos sin app.current_tenant_id
When se ejecuta SELECT * FROM users
Then el resultado es vacio
And no se produce error
And RLS previene acceso a datos
Notas Tecnicas
TenantGuard Implementation
// guards/tenant.guard.ts
@Injectable()
export class TenantGuard implements CanActivate {
constructor(
private dataSource: DataSource,
private tenantService: TenantService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user?.tenantId) {
throw new UnauthorizedException('Tenant no identificado');
}
// Verificar tenant activo
const tenant = await this.tenantService.findOne(user.tenantId);
if (!tenant || tenant.status !== 'active') {
throw new ForbiddenException('Tenant no disponible');
}
// Setear contexto de tenant en request
request.tenant = tenant;
request.tenantId = tenant.id;
return true;
}
}
TenantContext Middleware
// middleware/tenant-context.middleware.ts
@Injectable()
export class TenantContextMiddleware implements NestMiddleware {
constructor(private dataSource: DataSource) {}
async use(req: Request, res: Response, next: NextFunction) {
const tenantId = req['tenantId'];
if (tenantId) {
// Setear variable de sesion PostgreSQL para RLS
await this.dataSource.query(
`SET LOCAL app.current_tenant_id = '${tenantId}'`
);
}
next();
}
}
Base Entity con Tenant
// entities/tenant-base.entity.ts
export abstract class TenantBaseEntity {
@Column({ name: 'tenant_id' })
tenantId: string;
@ManyToOne(() => Tenant)
@JoinColumn({ name: 'tenant_id' })
tenant: Tenant;
@BeforeInsert()
setTenantId() {
// Se setea desde el contexto en el servicio
// No permitir override manual
}
}
// Uso en entidades
@Entity({ schema: 'core_inventory', name: 'products' })
export class Product extends TenantBaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
// ...
}
Base Service con Tenant
// services/tenant-aware.service.ts
export abstract class TenantAwareService<T extends TenantBaseEntity> {
constructor(
protected repository: Repository<T>,
@Inject(REQUEST) protected request: Request,
) {}
protected get tenantId(): string {
return this.request['tenantId'];
}
async findAll(options?: FindManyOptions<T>): Promise<T[]> {
return this.repository.find({
...options,
where: {
...options?.where,
tenantId: this.tenantId,
} as any,
});
}
async findOne(id: string): Promise<T | null> {
return this.repository.findOne({
where: {
id,
tenantId: this.tenantId,
} as any,
});
}
async create(dto: DeepPartial<T>): Promise<T> {
const entity = this.repository.create({
...dto,
tenantId: this.tenantId,
} as any);
return this.repository.save(entity);
}
}
Consideraciones de Performance
Indices Recomendados
-- Indice compuesto para queries filtradas por tenant
CREATE INDEX idx_users_tenant_email ON core_users.users(tenant_id, email);
CREATE INDEX idx_products_tenant_sku ON core_inventory.products(tenant_id, sku);
CREATE INDEX idx_orders_tenant_date ON core_sales.orders(tenant_id, created_at DESC);
-- Indice parcial para tenants activos
CREATE INDEX idx_users_active_tenant ON core_users.users(tenant_id)
WHERE deleted_at IS NULL;
Query Optimization
-- BUENO: Usa el indice compuesto
EXPLAIN ANALYZE
SELECT * FROM users
WHERE tenant_id = 'tenant-uuid' AND email = 'user@example.com';
-- MALO: Full table scan, luego filtro
EXPLAIN ANALYZE
SELECT * FROM users
WHERE email = 'user@example.com'; -- Sin tenant en WHERE, RLS agrega despues
Dependencias
| ID |
Descripcion |
| PostgreSQL 12+ |
Soporte completo de RLS |
| RF-TENANT-001 |
Tenants existentes |
| RF-AUTH-002 |
JWT con tenant_id |
Estimacion
| Tarea |
Puntos |
| Database: RLS policies |
5 |
| Database: Indices |
2 |
| Backend: TenantGuard |
3 |
| Backend: TenantContextMiddleware |
2 |
| Backend: TenantAwareService base |
3 |
| Backend: Tests de aislamiento |
5 |
| Total |
20 SP |
Historial
| Version |
Fecha |
Autor |
Cambios |
| 1.0 |
2025-12-05 |
System |
Creacion inicial |