Roles y permisos
El modelo de acceso de EMA Vault está diseñado para una audiencia muy específica: el staff interno de EMA Health. La autorización opera con cinco roles que reflejan la organización funcional del equipo, y la auditoría de acciones se apoya en un trigger SQL que captura cada mutación crítica con sanitización automática de secrets. La combinación produce un back-office donde el equipo trabaja con permisos coherentes con su área pero, simultáneamente, queda expuesto a accountability completo.
Los cinco roles internos
Sección titulada «Los cinco roles internos»Los cinco roles agrupan permisos por área funcional, no por jerarquía
organizacional. El rol que más diferencia hace es admin, que
opera como un bypass de los checks canRead/canWrite y queda
reservado para fundadores y staff con responsabilidad cruzada.
| Rol | Descripción | Bypass |
|---|---|---|
admin | Fundadores y staff con responsabilidad cruzada. | Sí — bypass todos los canRead/canWrite |
technical | Producto, infraestructura, audit, wiki. | No |
commercial | Pipeline, contratos, directorio, catálogo, pricing. | No |
finance | Finanzas, facturación electrónica, MRR, obligaciones. | No |
viewer | Lectura limitada (dashboards, wiki). | No |
La granularidad fina vive en la tabla vault_permissions(role, module, can_read, can_write), que el admin configura cuando un caso
particular requiere ajuste. Por ejemplo, si un comercial necesita
visibilidad sobre el módulo de finanzas para preparar una propuesta,
el admin puede agregar INSERT vault_permissions (role='commercial', module='finance', can_read=true, can_write=false) sin necesidad de
cambiarle el rol al usuario.
La matriz de permisos por módulo
Sección titulada «La matriz de permisos por módulo»La tabla siguiente resume el acceso default de cada rol a cada módulo. Las casillas marcadas con un guion indican que el módulo no aparece para ese rol; el rol viewer queda con acceso reducido a dashboards y lectura de wiki:
| Módulo | admin | technical | commercial | finance | viewer |
|---|---|---|---|---|---|
| dashboard | R+W | R | R | R | R |
| pipeline | R+W | — | R+W | R | — |
| organizations | R+W | — | R+W | R | R |
| tenants (clientes) | R+W | R | R+W | R+W | — |
| directory | R+W | — | R+W | R | R |
| contracts | R+W | — | R+W | R+W | — |
| catalog | R+W | — | R+W | R | — |
| catalog_costs | R+W | — | R | R+W | — |
| catalog_pricing | R+W | — | R+W | R+W | — |
| pricing-tool | R+W | — | R+W | R+W | — |
| finance | R+W | — | — | R+W | — |
| invoices (interna) | R+W | — | — | R+W | — |
| dte / fiscal | R+W | — | — | R+W | — |
| mrr | R+W | — | R | R+W | — |
| exchange | R+W | — | R | R+W | — |
| obligations | R+W | — | — | R+W | — |
| product | R+W | R+W | R | — | R |
| github sync | R+W | R+W | — | — | — |
| editorial | R+W | — | R+W | — | R |
| brand | R+W | — | R+W | — | R |
| checklist | R+W | R+W | R+W | R+W | — |
| time | R+W | R+W | R+W | R+W | — |
| documents | R+W | R+W | R+W | R+W | R |
| wiki | R+W | R+W | R | R | R |
| infra | R+W | R+W | — | — | — |
| analysis | R+W | R+W | R | R | — |
| notifications | R+W | R+W | R+W | R+W | R |
| changelog | R+W | R+W | R | — | R |
| audit | R+W | R | — | — | — |
| settings | R+W | — | — | — | — |
| emi-admin | R+W | — | — | — | — |
| emi-analytics | R+W | R | — | — | — |
El flujo de autenticación
Sección titulada «El flujo de autenticación»Vault solo acepta usuarios con correo @emahealth.io, restricción
que se aplica en dos puntos: a nivel de configuración del Google
OAuth client (que rechaza dominios externos antes de emitir token)
y a nivel de aplicación (que verifica existencia previa en
vault_users). El doble check es deliberado: si el OAuth se
configurara mal y aceptara dominios externos, el segundo filtro
seguiría protegiendo el sistema.
Una vez que el usuario pasa los dos filtros, el sistema actualiza el
last_login y el front-end recibe el set de permisos correspondiente
al rol. El sidebar dinámicamente esconde los módulos para los que el
rol no tiene can_read, lo que produce la sensación de que cada
usuario ve “su propio Vault” en lugar de un sistema con áreas
prohibidas.
Onboarding de nuevo staff
Sección titulada «Onboarding de nuevo staff»El onboarding de un usuario nuevo es siempre asíncrono: el admin lo
crea primero en la tabla vault_users, y solo después el usuario
puede entrar. La razón es la misma del doble filtro de
autenticación: evitar que un usuario externo pueda acceder por
accidente a través de un Google OAuth mal configurado.
El admin va a Equipo Vault y agrega al usuario con email,
nombre completo y rol. La creación dispara dos efectos: una fila en
vault_users con is_active=true y una notificación al usuario
invitándolo a entrar con Google. Si el usuario hace login antes de
estar registrado en vault_users, recibe un 403 con código
NO_VAULT_USER y el admin debe crearlo antes de reintentar.
Desactivación y cambio de rol
Sección titulada «Desactivación y cambio de rol»Cuando alguien deja el equipo o cambia de área, el procedimiento
estándar es actualizar la fila correspondiente en vault_users, no
eliminarla. La desactivación marca is_active=false y produce
pérdida inmediata de acceso, pero preserva el historial: los audit
logs, los contratos firmados, los leads atribuidos y las acciones
registradas siguen referenciando al user_id original. Borrar el
row rompería esas referencias y eliminaría trazabilidad histórica.
El cambio de rol funciona análogamente: el admin actualiza
vault_users.role y el usuario debe re-loguear (o esperar al refresh
del JWT) para que el sidebar se actualice con el nuevo set de
permisos.
Audit log cross-product
Sección titulada «Audit log cross-product»emashared.audit_log recibe eventos de tres fuentes. La primera y
más importante es el audit_trigger() SQL, que cubre las doce
tablas críticas en emavault y se ejecuta automáticamente en cada
INSERT, UPDATE o DELETE. La segunda son los logs cross-product
escritos manualmente por clinic, lab o well cuando ocurren acciones
con implicaciones cruzadas. La tercera son las acciones registradas
explícitamente por handlers en /api/audit/*.
La sanitización opera siempre antes de loggear: cualquier campo
cuyo nombre contenga password, token, secret, api_key,
private_key o aidbox_client_secret_encrypted se reemplaza por
[REDACTED]. Esto garantiza que el audit log nunca expone
credenciales aunque alguien las haya pasado por accidente en un
payload mutante.
-- Query típica: ¿quién canceló qué contrato la última semana?SELECT created_at, user_id, table_name, record_id, after->>'status'FROM emashared.audit_logWHERE table_name = 'contracts' AND action = 'UPDATE' AND after->>'status' = 'cancelled' AND created_at >= now() - interval '7 days'ORDER BY created_at DESC;Mecanismos de acceso especiales
Sección titulada «Mecanismos de acceso especiales»Más allá del flujo de autenticación estándar, Vault expone dos
mecanismos de acceso para casos específicos. El primero es el
header X-Platform-Key, que protege el endpoint
/api/internal/tenants y se usa exclusivamente para llamadas
worker-a-worker durante el provisioning de tenants. No requiere JWT,
pero la clave es secret rotable y queda fuera del alcance del
frontend. El segundo es el endpoint /api/github/webhook, que
valida HMAC sig (Sha256 del payload combinado con el secret) y se
usa para recibir notificaciones de GitHub sin necesidad de
autenticación humana.
Ninguno de estos canales emite respuestas con datos sensibles: el primero solo confirma operaciones y el segundo solo registra el evento recibido.
Aislamiento de datos sensibles
Sección titulada «Aislamiento de datos sensibles»Vault gestiona varios tipos de información que requieren distinto nivel de cuidado, y los aísla deliberadamente:
| Categoría | Ubicación | Acceso |
|---|---|---|
| Contratos firmados | emavault.contracts | finance + commercial |
| Datos financieros | emavault.finance_entries y mrr_entries | finance |
| Facturación electrónica y fiscal | emavault.fiscal_config y dte_emissions_log | finance |
| Aidbox secrets | emashared.tenant_aidbox_credentials (AES-GCM) | service-role only |
| Datos clínicos | NO en Vault — viven en clinic, lab y Aidbox per-tenant | nunca en Vault |
| Datos health Well | NO en Vault — proyecto Supabase separado | métricas agregadas only |
| Audit log | emashared.audit_log con sanitización | admin |
La regla más estricta es la de datos clínicos: Vault nunca los almacena. El médico, el TM o el director técnico operan dentro de su producto cliente (Clinic o Lab) y los datos clínicos viven en Aidbox con ORGBAC. Vault opera el negocio alrededor del producto, no el producto mismo. La separación es deliberada y garantiza que un incidente de seguridad en Vault no comprometa información clínica del paciente.