Roles, permisos y multi-tenancy
El modelo de acceso de EMA Lab responde a dos preguntas que aparecen constantemente en un laboratorio multi-cliente. La primera es de autorización: dado un usuario autenticado, qué puede hacer dentro de su laboratorio. La segunda es de aislamiento: cómo garantizar que los datos de un laboratorio jamás se filtren a otro y, dentro de un mismo laboratorio, cómo respetar las restricciones de visibilidad hacia clientes externos cuando corresponden. La autorización se resuelve con un modelo RBAC de seis roles, y el aislamiento se apoya en cuatro capas independientes que se refuerzan entre sí.
Los seis roles del laboratorio
Sección titulada «Los seis roles del laboratorio»EMA Lab reconoce los roles típicos de la operación clínica de un laboratorio latinoamericano. Cada uno agrupa permisos coherentes con la responsabilidad real del cargo y se separa explícitamente de los demás para hacer auditable la cadena de decisiones:
| Rol | Responsabilidad principal |
|---|---|
lab_admin | Administración total del tenant: configuración, usuarios, billing, conectores, pricing. |
lab_director | Director técnico o BQF a cargo. Firma clínica, define políticas (TAT, SLA, ruteo, exclusiones), firma reportes de stewardship. |
lab_tech | Tecnólogo médico o BQF de banca. Ingresa resultados, valida técnicamente, opera QC, microbiología y molecular. |
lab_reception | Recepciona órdenes, gestiona check-in, factura, atiende a clientes externos. |
lab_phlebotomist | Personal de toma de muestra (TENS, técnicos de toma). Etiqueta tubos y hace check-in en site. |
lab_viewer | Solo lectura sobre informes — útil para clínicos consultantes que necesitan ver resultados sin operar el LIS. |
A estos seis se suma un guard especial de plataforma. El
platform_admin no es un rol JWT tradicional sino un mecanismo de
acceso vía header X-Platform-Key que se usa exclusivamente para
operaciones de provisioning desde EMA Vault. No puede invocarse desde
el frontend ni desde un usuario humano: es un canal worker-a-worker
para la creación inicial del tenant.
La matriz de permisos por módulo
Sección titulada «La matriz de permisos por módulo»La tabla siguiente resume el acceso de cada rol a cada módulo del producto. La notación R+W indica acceso completo, R indica solo lectura, y un guion indica que el módulo no aparece para ese rol:
| Rol | Catálogo | Órdenes | Muestras | Worklists | Resultados | QC | Validación | Reportes | Micro | Molec | Stew | Inventario | Billing | Conectores | Config |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| admin | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W |
| director | R+W | — | — | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | — | — | R |
| tech | R | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | R+W | — | — | — |
| reception | R+W | R+W | R+W | — | — | — | — | R+W | — | — | — | — | R+W | — | — |
| phlebotomist | — | — | R+W | — | — | — | — | — | — | — | — | — | — | — | — |
| viewer | — | — | — | — | — | — | — | R | — | — | — | — | — | — | — |
platform_admin tiene R+W en todo, pero su uso queda restringido al
canal X-Platform-Key.
Multi-tenancy desde el lado del usuario
Sección titulada «Multi-tenancy desde el lado del usuario»Un mismo usuario puede tener varios roles en varios tenants y varios productos. Es la situación habitual de personal técnico que trabaja parte de la semana en un laboratorio y parte en una clínica relacionada, o de un director que supervisa varios laboratorios bajo una misma red:
SELECT user_id, tenant_id, product, role, is_tenant_adminFROM emashared.user_rolesWHERE user_id = 'cristian-uuid';
-- Resultado:-- | cristian-uuid | tenant-aurora-clinic | clinic | medico | false |-- | cristian-uuid | tenant-aurora-lab | lab | lab_admin | true |-- | cristian-uuid | tenant-andes-lab | lab | lab_director | false |El SPA mantiene en localStorage cuál es el tenant activo en la
sesión actual y un selector visible en la barra superior permite
cambiar entre tenants si el usuario tiene más de uno asignado. El
JWT que el frontend envía no incluye tenant_id: el
tenantMiddleware lo resuelve consultando user_roles con el JWT
del caller (no con service role, decisiones L099 y L102) lo que
garantiza que el tenant nunca puede falsearse desde el cliente.
ORGBAC: el aislamiento server-side
Sección titulada «ORGBAC: el aislamiento server-side»ORGBAC (Organization-Based Access Control) es el mecanismo que Aidbox
aplica del lado del servidor desde el sprint S9. Cada tenant tiene
su propia Organization en Aidbox, sus credenciales OAuth son
independientes (encriptadas en tenant_aidbox_credentials con
AES-GCM), y todas las queries FHIR pasan obligatoriamente por la URL
/Organization/{tenantId}/fhir/*. Si una query intenta acceder a
recursos de otro tenant, Aidbox la rechaza por policy server-side
sin importar el filtro que el código cliente haya aplicado.
Aunque alguien escapara los filtros de TypeScript por bug o por mal
manejo, Aidbox seguiría rechazando la query porque la URL ya está
scoped a la Organization correcta. A esta primera capa se suman
otras tres. La segunda es RLS de Supabase: cada tabla
operacional tiene la política tenant_id = (select tenant_id from user_roles where user_id = auth.uid() and product = 'lab'). La
tercera es el client isolation middleware, que veremos a
continuación. La cuarta es el tagging explícito con
meta.tag = urn:ema:tenant|<id> en cada recurso FHIR.
Client isolation: una capa adicional
Sección titulada «Client isolation: una capa adicional»Algunos laboratorios reciben órdenes de varias clínicas externas, y
es habitual que el director quiera restringir a ciertos TM solo a
las órdenes de clientes concretos —por ejemplo, dejar a un equipo
turno noche únicamente las órdenes de tres clínicas amigas, o
separar la operación de un convenio confidencial del resto del
laboratorio. EMA Lab modela esto con user_client_access:
-- Ejemplo: TM en lab-aurora pero solo procesa muestras de-- la clínica San José y la clínica BellavistaINSERT INTO emashared.user_client_access (user_id, tenant_id, client_org_fhir_id)VALUES ('tm-uuid', 'aurora-uuid', 'Organization/clinic-san-jose'), ('tm-uuid', 'aurora-uuid', 'Organization/clinic-bellavista');El clientIsolationMiddleware aplica el filtro automáticamente en
worklists, órdenes, reportes y billing. Cuando la tabla está vacía
para un usuario, el filtro se deshabilita y el usuario ve todos los
clientes del laboratorio. Cuando tiene filas, ve solo los listados.
Proveedores de autenticación
Sección titulada «Proveedores de autenticación»Cada tenant configura qué proveedores admite. Por defecto un laboratorio nuevo recibe email y magic link habilitados; los laboratorios que ya operan con Google Workspace o Microsoft 365 suelen activar el proveedor correspondiente para reducir fricción de acceso:
| Proveedor | Cuándo usarlo |
|---|---|
email | Email y contraseña, disponible siempre como fallback. |
magic-link | Link mágico por correo, sin contraseña, ideal para usuarios infrecuentes. |
google | Google OAuth, recomendado para administradores. |
microsoft | Microsoft 365 OAuth, equivalente para clínicas que viven en el ecosistema Microsoft. |
El frontend consulta GET /auth/resolve-tenant?slug=... antes de
pintar la pantalla de login para saber qué botones mostrar; eso
permite que dos tenants distintos tengan experiencias de login
diferentes en la misma instalación.
El portal del paciente
Sección titulada «El portal del paciente»Los pacientes acceden a sus resultados por una vía completamente
distinta de la del staff del laboratorio. El flujo combina el
identificador nacional del paciente (en Chile, el RUT), fecha
de nacimiento y correo electrónico para enviar un OTP que, una vez
validado, emite un token de portal con audiencia distinta a la del
staff (paciente), validez más corta (treinta minutos) y permisos
exclusivamente de lectura sobre los DR finales del paciente:
| Token | Audiencia | Validez | Permisos |
|---|---|---|---|
| Staff JWT | lab_admin/director/tech/reception/phlebotomist/viewer | ~1 h | RBAC según rol |
| Portal token | Paciente | 30 min | Read-only sobre sus propios DR finales |
| Hub token | Hub on-prem | rotable | Endpoints /api/hub/* |
El patientAuthMiddleware valida el portal token solo en las rutas
bajo /api/portal/*. La separación de audiencias es deliberada: el
código de staff y el de portal nunca comparten el mismo token, y el
modelo de permisos de uno no se filtra al otro por accidente.
Verificación pública de QR sin PII
Sección titulada «Verificación pública de QR sin PII»Cada DR firmado lleva un QR con un código de verificación único. Un médico que recibe el reporte —en papel, en email o vía HL7v2— puede confirmar autenticidad apuntando a la URL pública correspondiente:
https://docs.emahealth.io/portal/verify/:code?tenant={slug}El endpoint GET /api/portal/verify/:code es público, está
rate-limitado a treinta consultas por minuto, y no expone PII:
retorna fecha de emisión, laboratorio, hash de la firma y validez.
La idea es que la verificación sea pública y trivial sin filtrar
información del paciente.
Recorrido completo de una request
Sección titulada «Recorrido completo de una request»La secuencia que sigue un request desde el navegador del staff hasta los datos ilustra cómo encajan todas las capas:
1. Frontend: localStorage JWT → apiFetch() inyecta `Authorization: Bearer`2. authMiddleware: valida firma JWT + extrae user.id3. tenantMiddleware: SELECT user_roles WHERE user_id, tenant_id, product='lab' → rol4. clientIsolationMiddleware: filtra por user_client_access si aplica5. fhirFetch / buildEmaFhirClient: per-tenant FHIR client decryptado6. requireRole('admin', 'director', ...): RBAC gate, 403 si rol no en lista7. Handler: usa c.get('supabase'), c.get('tenantId'), c.get('role')8. Aidbox: ORGBAC valida path /Organization/{id}/fhir/*9. Supabase: RLS valida tenant_id + product='lab'Las primeras siete líneas ocurren en el worker y las dos últimas en los almacenes. Una request mal autorizada tiene nueve oportunidades distintas de ser rechazada antes de devolver datos.
Provisioning desde EMA Vault
Sección titulada «Provisioning desde EMA Vault»EMA Lab no expone UI propia para crear tenants. El provisioning se
hace desde EMA Vault vía un endpoint protegido por X-Platform-Key:
POST /api/platform/tenantsX-Platform-Key: <secret>{ "slug": "aurora", "country": "CL", "products": ["lab"], "admin": { "email": "ana@laboratorio.cl", "name": "Ana Pérez" }}Esa única llamada produce una cascada de seis efectos: se crea el
registro en tenants y tenant_products, se crea la Organization
raíz en Aidbox, se crea un OAuth client en Aidbox y su secret se
encripta con AES-GCM antes de guardarlo en
tenant_aidbox_credentials, se crea el usuario admin en
vault_users con el rol lab_admin y, por último, Loops envía el
email de invitación al admin del cliente. El laboratorio queda
operativo en cuestión de minutos.