# 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 { 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 { constructor( protected repository: Repository, @Inject(REQUEST) protected request: Request, ) {} protected get tenantId(): string { return this.request['tenantId']; } async findAll(options?: FindManyOptions): Promise { return this.repository.find({ ...options, where: { ...options?.where, tenantId: this.tenantId, } as any, }); } async findOne(id: string): Promise { return this.repository.findOne({ where: { id, tenantId: this.tenantId, } as any, }); } async create(dto: DeepPartial): Promise { 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 |