425 lines
12 KiB
Markdown
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 |
|