Saltar a contenido

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:

  1. registrar y autenticar usuarios;
  2. rotar y revocar sesiones mediante Redis;
  3. habilitar MFA TOTP;
  4. crear organizaciones y membresías;
  5. comprobar capacidades en el service;
  6. aislar organizaciones mediante RLS;
  7. 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, connections o channels dentro de deps.py.
  • Migraciones 0002+ de Shopify, analytics o webhooks.
  • Nombre de rol pulsecommerce_app.
  • URLs /v1 sin el prefijo global /api de TimeLiber Commerce.
  • Configuración, tokens, IDs o secretos del repositorio fuente.
  • init_db() con metadata.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_app creado por el provisioning privilegiado de infra/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_id y 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

  1. Dependencias y settings de secretos.
  2. Base, sesiones y transacción con SET LOCAL ROLE.
  3. Auth sin MFA y pruebas unitarias.
  4. Tenancy y capability matrix.
  5. Alembic 0001 y 0002.
  6. Prueba de integración RLS con PostgreSQL real.
  7. MFA y cifrado.
  8. OpenAPI y documentación generada.

Gates

  • ruff, mypy --strict y 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 = off ni asumir ownership.
  • Migraciones upgrade y downgrade se 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_rls y downgrade 0002 → 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 = off cerrado con error;
  • commerce_runtime_test y commerce_app sin SUPERUSER ni BYPASSRLS.

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/register crea la cuenta;
  • el frontend llama inmediatamente a POST /api/v1/auth/login y recibe cookies HttpOnly;
  • GET /api/v1/auth/me y POST /api/v1/auth/refresh restauran la sesión;
  • POST/GET /api/v1/organizations crean 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.