CreaRack-SL

Endpoint: GET /api/agent/local-token (Frontend recoge token local descifrado)

Resumen

Endpoint que permite al frontend web (usuario logueado) recuperar el token de autenticación local de un Agent perteneciente a su misma organización. El token se devuelve descifrado, listo para que el frontend lo use en requests a localhost:5050.

Aislamiento multi-tenant: solo usuarios de la misma organización que el Agent pueden acceder a su token.

Inerte en Fase 1: el frontend aún no lo llama.

Especificación

Ruta

GET /api/agent/local-token?agent_id=<agent_id>

Autenticación

  • Sesión de navegador (portero global): solo usuarios logueados.
  • Permiso requerido: fleet:view (ver detalles de flotas/agents).
  • No admite Agent JWT: aunque un Agent tuviera sesión, este endpoint rechaza Agent JWTs.

Query Parameters

  • agent_id (string, requerido): ID del Agent cuyo token se solicita.

Response

200 OK (éxito):

{
  "local_token": "<opaco>"  // token descifrado (era local_token_enc en BD)
}

403 Forbidden (sin sesión, sin permiso, o no es tu organización):

{
  "error": "Session authentication required"  // o "No organization in context"
}

404 Not Found (Agent no existe en tu org, o no ha depositado token aún):

{
  "error": "Agent not found"  // o "Agent has not registered a local token yet"
}

Implementación

Fichero

terminal/api/auth.py (líneas ~240–260, commit 61b0447).

Sub-componentes

  1. Portero global de sesión:

    • Verifica request.user logueado.
    • Rechaza requests anónimos (status 403).
  2. require_perm(request, "fleet", "view"):

    • Verifica que el usuario tenga permiso explícito.
    • Lanza excepción si falta (capturada por Django Ninja → 403).
  3. get_current_org(request):

    • Extrae la organización del usuario (o del contexto de la sesión).
    • Devuelve None si no hay org (response 403).
  4. Aislamiento por organización:

    agent = AgentInstance.objects.get(agent_id=agent_id, organization=org)
    • QuerySet filtrado por org del usuario (RLS efectivo).
    • Si el Agent pertenece a OTRA org, DoesNotExist → 404.
  5. Desciframiento:

    CredentialManager.decrypt_credential(agent.local_token_enc)
    • Usa la misma CredentialManager que cifró (Fernet simétrico).
    • Devuelve el token en claro (seguro: solo navegador web logueado lo recibe).
  6. Validación de existencia:

    • Si agent.local_token_enc está vacío, devuelve 404 (Agent no ha depositado token).

Flujo

  1. Frontend web (usuario logueado en sesión) quiere acceder al Agent local.
  2. Frontend envía GET /api/agent/local-token?agent_id=ag_xyz.
  3. Servidor verifica sesión + fleet:view + que ag_xyz sea del usuario.
  4. Servidor descifra AgentInstance.local_token_enc.
  5. Servidor responde con el token en claro.
  6. Frontend guarda el token en sesión/memoria y lo usa en requests a localhost:5050.

Seguridad

  • Multi-tenancy: QuerySet filtrado por organization (RLS).
  • Sesión requerida: Agent JWTs no autentican este endpoint.
  • Permiso granular: fleet:view (no admin).
  • Desciframiento solo en respuesta: el token nunca viaja cifrado al cliente (es opacodentro de la BD, descifrado en la response).
  • Logs sin exposición: no se registra el token en claro.

Testing

Cubierto en tests/api/test_terminal_local_token.py::TestGetLocalToken:

  • test_round_trip_register_then_fetch: Agent deposita, frontend recoge descifrado.
  • test_cross_tenant_isolation: admin de org B no puede leer token de agent en org A (404).
  • test_requires_session_not_agent_jwt: Agent JWT rechazado (403).
  • test_readonly_forbidden: usuario con permiso view_only rechazado (403).
  • test_token_not_yet_registered_404: Agent sin token registrado aún (404).

Fase 1 — Inerte

Este endpoint no es llamado por el frontend hasta la Fase 2, cuando se actualice la lógica de conexión local en la web.

Véase también

  • [[decision—20260610—terminal-auth-local-cross-origin]]
  • [[entity—terminal—endpoint—register-local-token]]
  • [[entity—terminal—model—agentinstance]]
  • [[concept—saas—multi-tenancy]]
  • [[feature—terminal—auth-local-agent-fase1]]