Files
whatsapp/modules/soporte/docs/arquitectura/30-roles-y-permisos.md
T
Lizandro GuarnizoandClaude Opus 5 018fb13332 Módulo Soporte: visor de documentación con renderizado Markdown y buscador
Nuevo módulo `soporte` con la documentación del proyecto en cuatro secciones:
manual de usuario (visible para todos), y documentación técnica, arquitectura
y operación (solo administradores).

- Markdown.php: renderizador propio del subconjunto que usa la documentación
  (encabezados, listas anidadas, tablas, código, citas). Escapa todo el texto
  antes de aplicar formato, así que los .md no pueden inyectar HTML. Se
  prefirió un archivo auditable a incorporar una dependencia externa.
- DocIndex.php: descubre los .md, arma el árbol, resuelve acceso por sección
  y construye el índice del buscador.
- Generadores.php: expande marcadores {{modulos}}, {{endpoints}}, {{tablas}},
  {{roles}} y {{servicios}} leyendo el código y la base en cada carga, para
  que los inventarios no puedan quedar desactualizados.

Se registra en SYSTEM_MODULES y se concede a los 12 roles con permission=read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:07:06 -05:00

4.3 KiB

Roles y permisos

Quién puede ver y hacer qué. Es el punto donde más seguido se cometen errores, así que conviene entenderlo completo.

Las dos columnas de un usuario

En admin_users conviven dos campos que parecen redundantes y no lo son:

Columna Para qué se usa
role Texto del rol (admin, bacteriologo, …). Lo consultan las vistas para decidir qué mostrar
role_id Apunta a roles.id. Es de donde se cargan los módulos al iniciar sesión

Hay que mantener las dos sincronizadas. Cambiar solo role deja al usuario con los permisos viejos, porque el acceso real sale de role_id. Este error ya ocurrió: un usuario cambió de rol, la interfaz mostraba el rol nuevo y los módulos seguían siendo los anteriores.

Al cambiar el rol de alguien, actualizá las dos a la vez:

UPDATE admin_users
   SET role = 'lab_recepcion',
       role_id = (SELECT id FROM roles WHERE slug = 'lab_recepcion')
 WHERE id = 12;

Cómo se arma el acceso al iniciar sesión

authenticateUser() (config/config.php) valida la contraseña y arma la sesión:

admin_users.role_id
   └── role_modules  →  lista de module_slug + permission
         └── $_SESSION['admin_user']['modules']              (qué módulos ve)
             $_SESSION['admin_user']['module_permissions']   (read o write en cada uno)

Esto ocurre una sola vez, al entrar. Cualquier cambio en role_modules no afecta a las sesiones abiertas: el usuario tiene que cerrar sesión y volver a entrar.

Las dos preguntas del control de acceso

hasModule('lab_domicilios')       // ¿puede entrar al módulo?
hasModuleWrite('lab_domicilios')  // ¿puede modificar, o solo mirar?
  • hasModule() mira si el slug está en la lista de módulos de la sesión.
  • hasModuleWrite() mira module_permissions[slug] === 'write'. Los administradores siempre pueden escribir.

Una vista típica lo usa así:

$puedeEscribir = hasModuleWrite('lab_domicilios');
...
<?php if ($puedeEscribir): ?><button>Nuevo domicilio</button><?php endif; ?>

La columna que manda es permission

role_modules tiene dos formas de expresar lo mismo, y solo una se usa:

Columnas ¿Se usan?
permission (read / write) Sí. Es lo que lee hasModuleWrite()
can_view, can_create, can_edit, can_delete, can_export No las lee el control de acceso

Poner can_edit = 0 no impide editar. Para dejar un módulo en solo lectura hay que fijar permission = 'read'. Las columnas can_* quedaron de un diseño anterior; conviene mantenerlas coherentes por prolijidad, pero no protegen nada.

Solo lectura de verdad:

UPDATE role_modules SET permission = 'read'
 WHERE role_id = 1030 AND module_slug IN ('lab_domicilios', 'lab_ordenes');

Roles actuales

{{roles}}

Sesiones sin role_id

Hay dos casos heredados que siguen contemplados en el código:

  • modules nulo y rol admin → acceso total. Cubre usuarios anteriores al sistema de roles.
  • Rol enfermero sin role_id → recibe enfermero_portal y lab_formularios de forma fija.

Verificaciones adicionales

El control por módulo no siempre alcanza. Varias pantallas agregan sus propias reglas:

Dónde Regla
enfermero_portal.php Solo admin, superadmin y enfermero
api/lab/save_domicilio.php Un enfermero solo edita domicilios que creó o que tiene asignados
api/lab/firmar_profesional.php Un enfermero solo firma envíos propios
modules/turnero/api/_helpers.php requireTurnero() en todos los endpoints del turnero
modules/turnero/module.php El menú cambia según rol y según la IP del equipo

Al agregar un endpoint que modifica datos, no alcanza con confiar en que la vista ocultó el botón: el endpoint tiene que verificar por su cuenta.

Diagnóstico rápido

Alguien reporta que no ve un módulo o que puede editar lo que no debería:

-- Qué rol tiene realmente y si las dos columnas coinciden
SELECT u.id, u.username, u.role, u.role_id, r.slug AS rol_real
  FROM admin_users u LEFT JOIN roles r ON r.id = u.role_id
 WHERE u.username = 'usuario';

-- Qué módulos le da ese rol
SELECT module_slug, permission FROM role_modules WHERE role_id = <role_id>;

Si los datos se ven bien y el usuario sigue sin acceso: no ha vuelto a iniciar sesión.