# 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 |
