Ir al contenido

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 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.

RolDescripciónBypass
adminFundadores y staff con responsabilidad cruzada. — bypass todos los canRead/canWrite
technicalProducto, infraestructura, audit, wiki.No
commercialPipeline, contratos, directorio, catálogo, pricing.No
financeFinanzas, facturación electrónica, MRR, obligaciones.No
viewerLectura 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 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óduloadmintechnicalcommercialfinanceviewer
dashboardR+WRRRR
pipelineR+WR+WR
organizationsR+WR+WRR
tenants (clientes)R+WRR+WR+W
directoryR+WR+WRR
contractsR+WR+WR+W
catalogR+WR+WR
catalog_costsR+WRR+W
catalog_pricingR+WR+WR+W
pricing-toolR+WR+WR+W
financeR+WR+W
invoices (interna)R+WR+W
dte / fiscalR+WR+W
mrrR+WRR+W
exchangeR+WRR+W
obligationsR+WR+W
productR+WR+WRR
github syncR+WR+W
editorialR+WR+WR
brandR+WR+WR
checklistR+WR+WR+WR+W
timeR+WR+WR+WR+W
documentsR+WR+WR+WR+WR
wikiR+WR+WRRR
infraR+WR+W
analysisR+WR+WRR
notificationsR+WR+WR+WR+WR
changelogR+WR+WRR
auditR+WR
settingsR+W
emi-adminR+W
emi-analyticsR+WR

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.

No

No

No

Login page

Click Google OAuth

¿email

@emahealth.io?

Reject — login fails

Supabase auth session

Vault Worker /auth/me

¿Existe en

vault_users?

403 NO_VAULT_USER

¿is_active?

403 inactive

Login ok + last_login update

Sidebar filtra por canRead

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.

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.

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.

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_log
WHERE table_name = 'contracts'
AND action = 'UPDATE'
AND after->>'status' = 'cancelled'
AND created_at >= now() - interval '7 days'
ORDER BY created_at DESC;

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.

Vault gestiona varios tipos de información que requieren distinto nivel de cuidado, y los aísla deliberadamente:

CategoríaUbicaciónAcceso
Contratos firmadosemavault.contractsfinance + commercial
Datos financierosemavault.finance_entries y mrr_entriesfinance
Facturación electrónica y fiscalemavault.fiscal_config y dte_emissions_logfinance
Aidbox secretsemashared.tenant_aidbox_credentials (AES-GCM)service-role only
Datos clínicosNO en Vault — viven en clinic, lab y Aidbox per-tenantnunca en Vault
Datos health WellNO en Vault — proyecto Supabase separadométricas agregadas only
Audit logemashared.audit_log con sanitizaciónadmin

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.