erp-core/docs/03-requerimientos/RF-tenants/RF-TENANT-003.md

425 lines
12 KiB
Markdown

# 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
```sql
-- 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
```sql
-- 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
```sql
-- 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
```gherkin
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
```gherkin
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
```gherkin
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
```gherkin
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
```gherkin
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
```gherkin
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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```sql
-- 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
```sql
-- 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 |