El control de acceso es propio, sin relaciones polimórficas. El código siempre consulta permisos, nunca nombres de rol: un rol es un grupo de permisos con un nivel en la jerarquía. Encima del rol, cada persona puede tener permisos concedidos o quitados.
Permisos efectivos = los del rol + los concedidos − los quitados (si chocan, gana el quitado).
roles
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
id | SMALLINT UNSIGNED | PK | |
code | VARCHAR(60) | UNIQUE, NOT NULL | requester, agent, supervisor, admin, super_admin |
name | VARCHAR(80) | NOT NULL | Nombre visible |
level | TINYINT UNSIGNED | NOT NULL, UNIQUE, CHECK BETWEEN 1 AND 100 | Nivel en la jerarquía: solo se administra a quien tiene un nivel menor. 100 es la cima (Super admin). Único, para que el orden nunca sea ambiguo |
is_system | BOOLEAN | NOT NULL, por defecto false | Uno de los cinco roles base: no se elimina (sí se edita) |
created_at, updated_at | TIMESTAMP |
permissions
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
id | SMALLINT UNSIGNED | PK | |
code | VARCHAR(60) | UNIQUE, NOT NULL | Formato módulo.acción[.alcance] |
name | VARCHAR(80) | NOT NULL | Nombre visible |
created_at, updated_at | TIMESTAMP |
role_permission
| Columna | Referencia | ON DELETE |
|---|---|---|
role_id | roles.id | CASCADE |
permission_id | permissions.id | RESTRICT |
PK (role_id, permission_id) e índice en permission_id.
user_role
| Columna | Referencia | ON DELETE |
|---|---|---|
user_id | users.id | CASCADE |
role_id | roles.id | RESTRICT |
PK (user_id, role_id) e índice en role_id. La tabla admite varios roles, pero la administración asigna uno por persona (ADR-0053).
user_permissions
Excepciones de permisos de una persona, encima de su rol.
| Columna | Tipo | Restricciones | Descripción |
|---|---|---|---|
user_id | BIGINT UNSIGNED | FK → users.id, CASCADE | Persona; la excepción se va con la cuenta |
permission_id | SMALLINT UNSIGNED | FK → permissions.id, RESTRICT | Permiso concedido o quitado |
effect | VARCHAR(5) | NOT NULL, CHECK IN ('grant', 'deny') | grant: lo tiene aunque su rol no lo traiga; deny: no lo tiene aunque su rol lo traiga |
granted_by | BIGINT UNSIGNED | FK → users.id, RESTRICT | Quién dio la excepción; no se borra a quien administró |
created_at | TIMESTAMP | NOT NULL |
PK (user_id, permission_id), e índices en permission_id y granted_by. Una persona tiene a lo sumo una excepción por permiso.
Matriz inicial
| Permiso | Solicitante | Agente | Supervisor | Administrador | Super admin |
|---|---|---|---|---|---|
incidents.view.own, incidents.create, incidents.update.own | ✓ | ✓ | ✓ | ✓ | ✓ |
incidents.view.assigned, incidents.view.unassigned, incidents.take, incidents.status.assigned | ✓ | ✓ | ✓ | ||
incidents.view.any, incidents.update.any, incidents.assign, incidents.status.any | ✓ | ✓ | |||
incidents.delete, incidents.restore, incidents.export | ✓ | ✓ | |||
incidents.reopen.request | ✓ | ✓ | ✓ | ✓ | ✓ |
incidents.reopen.approve | ✓ | ✓ | |||
incidents.comment | ✓ | ✓ | ✓ | ✓ | ✓ |
dashboard.view.own | ✓ | ✓ | ✓ | ||
dashboard.view.any | ✓ | ✓ | |||
audit.incidents.view | ✓ | ✓ | |||
audit.admin.view | ✓ | ✓ | |||
users.view, users.create, users.update, users.activate, users.roles.assign, users.permissions.grant | ✓ | ✓ | |||
users.mfa.reset, users.access.reset | ✓ | ||||
roles.manage | ✓ | ||||
system.logs.view, system.health.view, system.actions.execute | ✓ | ||||
| Nivel | 20 | 40 | 60 | 80 | 100 |
Es la matriz con la que nace cada rol base. Después la administración la puede ajustar desde la pantalla de roles (y crear roles nuevos entre los niveles), y volver a sembrar no la revierte (ADR-0054).
Dónde está en el código
- Migraciones:
000002a000005,000023(nivel),000025(user_permissions),000028(quitausers.manage) ,000029(roles de sistema y nivel único) y000031(divideaudit.view) - Modelos:
Role.phpyPermission.php - Permisos efectivos:
UserPermissions.phpyPermissionSql.php - Matriz:
AccessControlSeeder.php - Decisiones: ADR-0021, ADR-0053 , ADR-0054 y ADR-0056