Trasplante de identidad y multi-tenancy
Estado: Implementado, cubierto automáticamente y validado en despliegue persistente
Fecha: 2026-08-31
Fuente: /home/admin_/software/freelance/apps/api
Este documento concreta el ADR 0001 para el primer bloque de código. PulseCommerce permanece en sólo lectura.
Resultado buscado
Antes de incorporar Shopify, Commerce debe poder:
- registrar y autenticar usuarios;
- rotar y revocar sesiones mediante Redis;
- habilitar MFA TOTP;
- crear organizaciones y membresías;
- comprobar capacidades en el service;
- aislar organizaciones mediante RLS;
- demostrar con PostgreSQL real que un tenant no ve ni escribe datos de otro.
Inventario de reutilización
Core
| Fuente | Decisión | Adaptación requerida |
|---|---|---|
src/core/security.py |
Adaptar | Renombrar settings, conservar Argon2id, JWT tipado y expiraciones. |
src/core/identity.py |
Reutilizar | Contrato mínimo sin dependencia del dominio auth. |
src/core/authorization.py |
Adaptar | Conservar capabilities y GUCs; usar nombres Commerce y transacciones runtime. |
src/core/database.py |
Reescribir sobre el esqueleto | Añadir Base, timestamps, sessionmaker y rol runtime sin perder health checks. |
src/api/deps.py |
Reescribir por composición | Incorporar sólo auth/tenancy; excluir Shopify, analytics y conexiones. |
src/api/exception_handlers.py |
Adaptar | Registrar únicamente excepciones presentes. |
Dominio auth
| Fuente | Decisión | Motivo |
|---|---|---|
contracts.py |
Reutilizar | Mantiene services desacoplados de SQLAlchemy. |
models.py |
Adaptar | Modelo válido; los constraints definitivos pertenecen a Alembic. |
schemas.py |
Adaptar | Mantener requests/responses separados y evitar secretos. |
repository.py |
Reutilizar/adaptar | Queries select() y escrituras encapsuladas. |
service.py |
Reutilizar/adaptar | Lógica pura, rotación de refresh y respuestas genéricas. |
session_store.py |
Adaptar | Namespace Redis propio de Commerce. |
password_policy.py |
Evaluar y reutilizar | Política madura; confirmar mensajes y tests antes de copiar. |
mfa.py, mfa_service.py |
Adaptar | Requiere cifrado Fernet y secreto de configuración propio. |
routers/* |
Adaptar | Prefijo canónico /api/v1, dependencias mínimas y handlers globales. |
Dominio tenancy
| Fuente | Decisión | Motivo |
|---|---|---|
contracts.py |
Reutilizar | Contrato pequeño y estable. |
models.py |
Adaptar | Mantener organization_id, UUIDs y roles; revisar soft delete. |
schemas.py |
Adaptar | Contratos HTTP mínimos para fundación. |
repository.py |
Reutilizar/adaptar | Debe filtrar por usuario y organización explícitamente. |
service.py |
Adaptar | Service no importará FastAPI ni SQLAlchemy. |
router.py |
Adaptar | Requiere principal autenticado y dependencias Commerce. |
Lo que no se traslada
- Dependencias de
analytics,connectionsochannelsdentro dedeps.py. - Migraciones
0002+de Shopify, analytics o webhooks. - Nombre de rol
pulsecommerce_app. - URLs
/v1sin el prefijo global/apide TimeLiber Commerce. - Configuración, tokens, IDs o secretos del repositorio fuente.
init_db()conmetadata.create_all()como mecanismo de producción; Alembic es la única autoridad del esquema.
Nueva secuencia de migraciones
0001_identity_tenancy
users.organizations.organization_members.- UUIDs, timestamps, constraints e índices.
- Columnas MFA en el esquema inicial; no se conserva una migración histórica artificial.
0002_tenant_rls
- Usar el rol grupal
commerce_appcreado por el provisioning privilegiado deinfra/core; Alembic no intenta crear roles. - Aplicar
ENABLE/FORCE ROW LEVEL SECURITYúnicamente a tablas existentes. - Política de organizaciones basada en
app.current_user_idy membresía. - Política de membresías basada en usuario/organización.
- Grants mínimos al rol de aplicación.
El usuario de conexión runtime será miembro de commerce_app, pero no propietario de tablas. La credencial de migraciones no se usará para servir requests.
infra/core provisiona el motor compartido y los tres roles: commerce_owner posee commerce_db; commerce_runtime sólo conecta y puede asumir commerce_app; commerce_app es NOLOGIN y recibe únicamente grants de Alembic. Ninguno tiene SUPERUSER o BYPASSRLS.
Los scripts en infra/postgres/init/ sólo aplican a un volumen nuevo. En un clúster existente, crear o verificar esos roles exige un procedimiento explícito y auditado; no se ejecuta desde la aplicación ni desde una migración.
Orden de implementación
- Dependencias y settings de secretos.
- Base, sesiones y transacción con
SET LOCAL ROLE. - Auth sin MFA y pruebas unitarias.
- Tenancy y capability matrix.
- Alembic
0001y0002. - Prueba de integración RLS con PostgreSQL real.
- MFA y cifrado.
- OpenAPI y documentación generada.
Gates
ruff,mypy --stricty tests unitarios verdes.- Cuatro casos canónicos por endpoint: éxito, validación, auth y regla de negocio.
- Contraseña y tokens nunca aparecen en respuestas o logs.
- Refresh token rotado no puede reutilizarse.
- Un no-miembro recibe respuesta genérica y RLS devuelve cero filas.
- Un runtime role no puede ejecutar
SET row_security = offni asumir ownership. - Migraciones
upgradeydowngradese validan en PostgreSQL desechable.
Riesgo pendiente
La fuente utiliza el nombre de configuración organization_id, mientras la regla global admite tenant_id o property_id. Commerce conservará organization_id en el modelo de identidad, pero toda tabla comercial futura deberá llevar explícitamente ese foreign key y tratarlo como tenant boundary. No se introducirán ambos nombres para el mismo concepto.
Evidencia de validación — 2026-08-31
La prueba tests/integration/test_tenant_rls.py ejecutó sobre PostgreSQL 17 efímero:
- upgrade
base → 0002_tenant_rlsy downgrade0002 → base; - dos organizaciones, con membresía sólo en una;
- lectura del runtime limitada a la organización propia;
- intento de
SET LOCAL row_security = offcerrado con error; commerce_runtime_testycommerce_appsinSUPERUSERniBYPASSRLS.
Resultado: 1 passed. Esta evidencia valida las migraciones en un contenedor desechable; no afirma que hayan sido aplicadas en la base persistente.
Flujo HTTP y frontend incorporado — 2026-08-31
Se adaptó el patrón de PulseCommerce sin copiar su gestión de bearer tokens:
POST /api/v1/auth/registercrea la cuenta;- el frontend llama inmediatamente a
POST /api/v1/auth/loginy recibe cookies HttpOnly; GET /api/v1/auth/meyPOST /api/v1/auth/refreshrestauran la sesión;POST/GET /api/v1/organizationscrean y listan las empresas del usuario;GET /api/v1/organizations/{organization_id}sólo devuelve una empresa del miembro;- si no existe una empresa, el dashboard exige crearla antes de mostrar integraciones;
- si existen varias, un selector determina la empresa activa;
- el UUID nunca se escribe manualmente ni se toma como prueba de autorización.
La API mantiene Router → Service → Repository; la membresía se comprueba en
aplicación y RLS permanece como segunda barrera. Las pruebas HTTP pasan con
dependencias aisladas y la suite backend completa queda verde. El frontend usa
React Hook Form, Zod, TanStack Query, Zustand y un cliente tipado generado desde
OpenAPI, siguiendo el mismo patrón de experiencia de PulseCommerce pero con el
contrato de cookies propio de TimeLiber Commerce.
Evidencia operativa HTTPS — 2026-08-31
Sobre commerce.timeliber.com.co se verificó el journey completo:
| Operación | Resultado esperado y observado |
|---|---|
| Registro | 201 |
| Login y cookies seguras | 200 |
| Sesión actual | 200 |
| Crear primera empresa | 201 |
| Listar empresas propias | 200 |
| Logout y revocación | 204 |
| Reutilizar sesión cerrada | 401 |
Durante el primer smoke se detectó que logout devolvía una instancia Response
sin status efectivo bajo la versión runtime de FastAPI/Starlette. Se corrigió
para que FastAPI construya el 204, se añadió una regresión HTTP y se repitió
el journey completo antes de desplegar el frontend.
Endurecimiento posterior a auditoría
La auditoría multi-tenant de 2026-08
cerró cuatro brechas de defensa en profundidad: autoafiliación mediante RLS,
membresías soft-deleted visibles en política, relaciones de integración sin FK
tenant-compuesta y caché frontend entre cuentas. La migración correspondiente
es 0005_tenant_hardening.