Files
message_bus/reports/0008-2026-06-07-unibus-admin-users-wired.md
T
egutierrez d43ffae3ae chore: auto-commit (17 archivos)
- reports/0001-2026-06-07-unibus-grafana-monitoring.md
- reports/0008-2026-06-07-unibus-admin-users-wired.md
- reports/0008-2026-06-07-unibus-decentralization-audit.md
- reports/0009-2026-06-07-unibus-cluster-hardening.md
- reports/0010-2026-06-07-unibus-android-native.md
- reports/0011-2026-06-07-unibus-cluster-deploy.md
- reports/0012-2026-06-07-unibus-deploy-gaps-closed.md
- reports/0013-2026-06-07-unibus-admin-panel.md
- reports/0014-2026-06-07-unibus-users-http-admin-api.md
- reports/0015-2026-06-07-unibus-web-wired.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 01:57:00 +02:00

8.0 KiB

Report 0008 — unibus_admin: pestaña Users cableada a la API del plano de control

  • Fecha: 07/06/2026
  • Autor: agente (Claude Opus 4.8)
  • Ámbito: projects/message_bus/apps/unibus_admin (gateway Go + SPA Mantine), sub-repo dataforge/unibus_admin, rama master
  • Estado: done (código + verificación E2E del código) — con un gap de despliegue del bus documentado abajo

Resumen

La pestaña Users del panel de administración de unibus estaba degradada en cluster porque el gateway sólo sabía gestionar la allowlist abriendo el membership.Store directo con --db, y caía a users_backend=none (estado informativo) cuando no había acceso directo al store. El control plane de unibus ahora expone una API HTTP admin-only de users, y pkg/client la envuelve con ListUsers/AddUser/RevokeUser firmados. Este trabajo cablea el gateway a esos métodos (camino principal: API HTTP firmada, funciona en cluster), deja la pestaña Users operativa (sin el estado "degradado"), y verifica la cadena completa list → add → revoke (con idempotencia 409) end-to-end contra un membershipd master real. --db queda como fallback single-node explícito.

Cambios

Archivo Cambio
internal/admin/repo.go Eliminado ErrUsersUnavailable y el camino "none"; comentarios del interface Repo y de MeInfo.UsersBackend actualizados ("control-plane" | "sqlite").
internal/admin/repo_bus.go ListUsers/AddUser/RevokeUser usan r.cli.* (API firmada) cuando no hay store directo; con --db siguen usando el store. UsersWritable()true (el gateway conectado siempre puede; el control real es requireAdmin en runtime). Backend por defecto "control-plane", "sqlite" con --db.
internal/admin/server.go Handlers /api/users simplificados (cualquier error del control plane → 502 con su mensaje, que ya trae el código embebido, p.ej. (HTTP 409)); quitado el manejo de ErrUsersUnavailable y el import errors.
main.go --db documentado como opcional (fallback single-node); por defecto el backend es control-plane. Logs de arranque acordes.
web/src/pages/UsersPage.tsx Quitado el Alert "Gestión de users no disponible" y el gating writable (botón Add deshabilitado / revoke oculto). El badge muestra backend: <control-plane|sqlite>. La tabla (handle/rol/estado/sign_pub/creado), el modal Add (handle + sign-pub hex64 + rol) y revoke con confirmación se mantienen.
web/dist/* Bundle SPA recompilado (embebido por el binario).
app.md Diagrama, tabla de capacidades, ejemplos de arranque y gaps actualizados al cableado control-plane + el gap de despliegue del bus.

El gateway reutiliza los métodos del cliente del bus (pkg/client), no reimplementa firma ni HTTP. No se tocó el repo unibus (sólo se importa por replace => ../unibus).

Verificación

Mecánica (verde)

go vet ./...        → VET_EXIT:0
go build -o unibus_admin .   → BUILD_EXIT:0  (19 MB, embebe la SPA)
cd web && pnpm build         → tsc -b + vite build OK (sin errores de tipo)

Cobertura E2E del código (verde) — contra membershipd master real

El cluster de producción aún no expone la ruta /users (ver Gaps), así que la cadena del panel se verificó contra un membershipd compilado de master (con la ruta) arrancado localmente (sqlite, --bus-auth soft), con el operador sembrado como admin (membershipd user add --role admin). El gateway firma como el operador; el bus verifica con requireAdmin. Secuencia ejecutada por la API del panel (/api/users):

Paso Comando (gateway en :18481) Resultado
list (antes) GET /api/users [operator(admin,active)]
add POST /api/users {gapcheck_admin_ui, member} 201 {"status":"added"}
list GET /api/users gapcheck_admin_ui active/member + operator
re-alta (idempotencia) POST /api/users (mismo sign_pub) 409 user already registered (unchanged); revoke it first
revoke POST /api/users/revoke {sign_pub} 200 {"status":"revoked"}
list (final) GET /api/users gapcheck_admin_ui revoked + revoked_at; operator intacto

sign_pub de prueba: 1cb658d9d3f23e6bf5791a3865a1eec731f3df30ad4b85f6fca0074b8bdc98a4 (aleatorio, role member). Limpieza: revocado al final (revoke es la limpieza; no hay hard delete). El operador admin y la DB de prueba vivían en /tmp efímero, ya borrados. No se tocó la allowlist del cluster de producción.

SPA (verde) — verificada visualmente (browser MCP)

Panel servido en :18481, pestaña Users:

  • Badge backend: control-plane (sin Alert degradado), botón "Añadir user" activo.
  • Tabla con gapcheck_admin_ui MEMBER/REVOKED (sin botón revoke por estar revocado) y operator ADMIN/ACTIVE (con botón revoke).
  • Modal "Añadir user al bus" renderiza Handle + sign_pub (hex, 64) + Rol(member) + Cancelar/Añadir.

Conectividad contra el cluster vivo (verde, salvo la ruta ausente)

  • Gateway local apuntado a homer (141.94.69.66:8470) + datardos (51.91.100.142:8470), TLS+nkey con la CA del bus, identidad operator desde pass: conecta y firma (/api/me devuelve el endpoint real del operador vI8HXcintzK-…). GET /api/users404 page not found desde el control plane (ruta ausente en esos binarios).
  • Panel efímero arrancado en magnus (puerto 18482, sin tocar el unibus-admin.service) contra el control plane loopback de magnus (KV, enforce, TLS): /api/me OK (pasa el enforce de magnus firmando como operador), GET /api/users404. El binario efímero y su log se borraron; el service real quedó active.

Estado del deploy en magnus

No se re-desplegó el service. El panel unibus-admin.service sigue active en magnus (healthz 200). Build linux/amd64 del binario nuevo: listo (probado en magnus en puerto efímero). El re-deploy queda bloqueado por una dependencia: re-desplegar ahora cambiaría la pestaña Users de un Alert informativo a un error 502 (GET /users → 404), porque el membershipd de magnus aún no expone la ruta. Procedimiento para cuando el bus esté en v0.10.0 (aditivo, sin tocar otros sites del Caddyfile):

cd web && pnpm install && pnpm build && cd ..
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o unibus_admin .
scp unibus_admin magnus:/opt/unibus_admin/unibus_admin
ssh magnus 'systemctl restart unibus-admin.service'   # el unit ya arranca sin --db (backend control-plane)
ssh magnus 'curl -fsS http://127.0.0.1:8480/healthz'  # + comprobar la pestaña Users vía el subdominio Caddy

El deploy/unibus-admin.service no necesita cambios para el camino control-plane (no usa --db). Verificar tras el restart que ExecStart no incluye --db (si lo incluyera, quitarlo para usar la API del plano de control).

Gaps / pendientes

  1. Cluster sin la ruta /users (despliegue). Los membershipd desplegados (magnus/homer/datardos) son anteriores al merge de la API de users y devuelven 404. La verificación E2E del panel contra el cluster vivo de producción no es posible hasta actualizar el bus a v0.10.0. La cadena está verificada contra un membershipd master real (mismo código que correrá en el cluster), y el gateway ya conecta y firma bien contra los nodos reales. Acción fuera de este aislamiento (toca el repo unibus/su deploy): actualizar los binarios del cluster.
  2. Mapeo de código de estado. Un 409 (re-alta idempotente) del control plane llega a la SPA como 502 con el mensaje del bus (que incluye (HTTP 409)). La SPA muestra el mensaje íntegro, así que la UX es correcta y accionable; no se parseó el string de error para propagar el código exacto (se evitó un parser frágil — KISS). Mejora menor opcional si pkg/client expone el status estructuradamente.
  3. Replicación followers 2/2. No se inspeccionó la replicación KV cross-node porque la ruta /users no está viva en el cluster; el panel no expone métricas de JetStream (varz/jsz) — gap ya conocido de la pestaña Cluster.