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

114 lines
4.3 KiB
Markdown

# 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:
```sql
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
```php
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í:
```php
$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:
```sql
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:
```sql
-- 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**.