Saltar a contenido

Aislamiento por inquilino

Esta página describe cómo Hardhat Flow hace cumplir que todo usuario que no sea administrador pertenezca a exactamente un inquilino, cómo se resuelve request.tenant, y el contrato /api/me/ que el frontend usa al iniciar sesión.


El invariante

Todo registro User debe cumplir una de dos condiciones:

Condition Allowed
user.role == platform_admin y user.company_id es None Platform Admin — no se requiere inquilino
user.company_id está definido (cualquier otro rol) ✓ Usuario de inquilino
Cualquier otro rol con user.company_id == None ✗ Rechazado en autenticación

Esto se aplica en la capa de autenticación de DRF, no en vistas o serializers individuales.


Flujo de autenticación

Auth0JWTAuthentication (en apps/accounts/authentication.py) maneja cada solicitud API:

  1. Se verifica el JWT de Auth0.
  2. Se llama a _get_or_create_user(payload):
  3. Si el JWT no trae reclamo de rol → lanza AuthenticationFailed(code="no_tenant").
  4. Crea o carga el registro local User con el rol resuelto.
  5. Si user.role != platform_admin y user.company_id is None → lanza AuthenticationFailed(code="no_tenant").
  6. Se llama a _set_request_tenant(request, user):
  7. Platform Admins: el slug de inquilino en actuación (encabezado X-Tenant-Slug o parámetro ?acting_tenant=) se consulta primero. Si hay un slug válido, request.tenant se establece a esa empresa aunque el administrador también tenga company_id. Si no hay slug pero user.company_id está definido, se usa esa empresa. Si ninguno está disponible, request.tenant = None (los endpoints de administración que no necesitan inquilino siguen funcionando).
  8. Todos los demás roles: el usuario queda fijado exclusivamente a user.company. Los encabezados de inquilino en actuación y los parámetros de consulta se ignoran, evitando acceso entre inquilinos.

TenantMiddleware es ahora solo un wrapper de limpieza — borra el estado de inquilino en hilo local tras cada respuesta. Ya no lee el slug de la URL ni hace búsqueda de inquilino.


Forma de la respuesta de error

Cuando la autenticación falla porque el usuario no tiene inquilino, la respuesta es:

HTTP 401 Unauthorized
Content-Type: application/json

{
  "detail": "Your account is not associated with a company. Please contact your administrator to request access.",
  "code": "no_tenant"
}

El frontend usa la rama code === "no_tenant" para redirigir a /account-error/ en lugar de la página genérica de inicio de sesión.


Endpoint /api/me/

GET /api/me/ devuelve el perfil del usuario autenticado. Requiere autenticación pero no requiere inquilino — funciona tanto para Platform Admins como para usuarios de inquilino.

Respuesta (200 OK):

{
  "id": "uuid",
  "email": "[email protected]",
  "role": "owner",
  "is_admin": false,
  "company": {
    "id": "uuid",
    "slug": "acme-construction",
    "name": "Acme Construction LLC"
  }
}

Para Platform Admins, company suele ser null e is_admin es true.

Si un Platform Admin envía X-Tenant-Slug: {slug} en la solicitud (y el slug coincide con una empresa no eliminada), la autenticación configura la base de datos de ese inquilino para la solicitud y GET /api/me/ devuelve esa empresa en el objeto company para que la interfaz coincida con el inquilino activo. El mismo slug puede pasarse como parámetro de consulta acting_tenant (por ejemplo GET /api/v1/bids/?acting_tenant=acme); el encabezado gana cuando ambos están presentes. Los rewrites locales de Next.js pueden no reenviar encabezados personalizados a Django, por eso la aplicación usa acting_tenant como respaldo en desarrollo. Un slug desconocido o eliminado produce 401 con code: "unknown_tenant_slug".

El slug de inquilino en actuación tiene precedencia sobre el company_id propio del administrador. Así el flujo “Open as Tenant” funciona incluso cuando el usuario Platform Admin tiene company_id en su fila — el slug de la solicitud siempre gana para Platform Admins. Los usuarios que no son administradores nunca honran este encabezado ni parámetro; su inquilino siempre proviene de user.company_id.

Si el usuario no tiene inquilino (y no es Platform Admin), la clase de autenticación rechaza la solicitud antes de que corra la vista — la respuesta es el 401 {"code": "no_tenant" descrito arriba, no un 200 con company: null.


Flujo del frontend tras iniciar sesión

Tras el redirect de Auth0 a la aplicación en /post-login/:

  1. El hook useCurrentUser obtiene GET /api/me/.
  2. Si tiene éxito:
  3. is_admin: true → redirección a /platform-admin/tenants/
  4. de lo contrario → redirección a /dashboard/
  5. En 401 con code === "no_tenant" → redirección a /account-error/
  6. En cualquier otro error → redirección a /login/

La página /account-error/ explica que la cuenta aún no está asociada a una empresa y ofrece un botón Sign out.

El layout de la aplicación (frontend/app/(app)/layout.tsx) también observa el error de useCurrentUser y redirige a /account-error/ si code === "no_tenant" en navegaciones posteriores — protege contra renovación de token tras desaprovisionar una cuenta en mitad de sesión.


Usuarios existentes sin inquilino

Los usuarios cuyos registros existen en la BD con company_id = NULL (y cuyo rol no es platform_admin) recibirán el error no_tenant en su próximo inicio de sesión. Llegan a /account-error/ y no pueden acceder a endpoints API de inquilino hasta que un Platform Admin los asigne a una empresa.

No se ejecuta migración de datos automática. Si desea que los usuarios sin inquilino sean visibles para administradores, cree registros AccessRequest manualmente o asigne una empresa mediante la consola Django admin en /django-admin/.