erp-core/docs/01-fase-foundation/MGN-004-tenants/requerimientos/RF-TENANT-003.md

12 KiB

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