La administración funcional de usuarios, roles, recursos y acciones se explica en Usuarios, roles, permisos, acciones y triggers. La consulta específica de aprobadores está en Usuarios aprobadores.
Describir cómo fluye la autorización desde la configuración de roles en Maestros hasta los controles de una pantalla Angular. La autorización se configura en el módulo de Maestros y se consume en la aplicación mediante AuthorizationStore y, cuando corresponda, ActionGateAdapter.
El frontend usa permisos para adaptar la experiencia y bloquear interacciones. La API backend debe validar cada operación protegida; ocultar un botón nunca reemplaza la autorización del servidor.
La configuración conecta estos elementos:
Usuario → Roles asignados → Recursos/opciones del rol → Acciones permitidas
↓
Código de acción + recurso/ruta
↓
AuthorizationStore / ActionGateAdapter en frontend
↓
Pantalla principal → permiso efectivo → componentes hijos
CREATE, EDIT, DELETE o VIEW.La decisión efectiva depende de la configuración entregada por la plataforma y consultada por el frontend. No se debe inferir autorización a partir del nombre visible del rol, recurso, botón o acción: usar los códigos y contratos configurados.
La administración de permisos ocurre en Maestros, no en el componente de negocio:
El permiso configurado para el rol es la fuente administrativa. Los componentes no deben inventar roles ni mantener una lista local paralela de permisos.
AuthorizationStore se importa desde @nebula-shared/data-access. La pantalla principal consulta la acción contra el recurso/ruta actual y crea señales derivadas para usar en la plantilla y en handlers.
Ejemplo orientativo, siguiendo el patrón de invoicing-concepts-revenue:
private readonly authorization = inject(AuthorizationStore);
private readonly currentUrl = window.location.pathname;
readonly canCreate = computed(() =>
this.authorization.can(this.currentUrl, 'CREATE'),
);
readonly canEdit = computed(() =>
this.authorization.can(this.currentUrl, 'EDIT'),
);
readonly canDelete = computed(() =>
this.authorization.can(this.currentUrl, 'DELETE'),
);
readonly canView = computed(() =>
this.authorization.can(this.currentUrl, 'VIEW'),
);
readonly canEditForm = computed(() =>
this.canEdit() && this.pageMode() !== 'view',
);
Usar el recurso/ruta que corresponde al código configurado en Maestros. Confirmar cómo AuthorizationStore.can() normaliza la ruta y resuelve la opción antes de introducir rutas manuales o conversiones locales.
Mantener una sola señal por permiso efectivo. Evitar consultar el mismo par ruta/acción varias veces dentro de computeds redundantes. Si una acción adicional requiere otro código configurado, declararla explícitamente y usar ese código exacto.
La señal debe controlar qué acción se presenta:
acciones(): ActionConfig<Entidad>[] {
return [
{
label: 'Editar',
action: 'edit',
visible: () => this.canEdit(),
},
];
}
Y el handler debe volver a comprobar el permiso, pues las acciones pueden ejecutarse desde otras rutas de UI o eventos:
onTableAction(event: ActionEvent<Entidad>): void {
if (event.action === 'edit' && !this.canEdit()) return;
// continuar con la acción autorizada
}
Para operaciones CRUD, aplicar el permiso también a las acciones del encabezado y a las transiciones a create, edit o view.
La pantalla contenedora debe ser la fuente del permiso. Calcularlo allí y enviarlo a las secciones que muestran controles editables:
<app-detalle
[form]="form"
[canEdit]="canEditForm()"
/>
El hijo recibe el permiso como input y lo aplica a acciones y mutaciones:
canEdit = input(false);
agregarFila(): void {
if (!this.canEdit()) return;
// modificar el estado local o formulario y emitir el cambio
}
La plantilla del hijo debe ocultar o deshabilitar las acciones de edición si canEdit() es falso. No usar computed(() => true) ni volver a consultar el store de autorización desde cada hijo, porque eso duplica la resolución de ruta y puede producir criterios distintos. Para controles que requieren permisos distintos, pasar señales separadas, como canAdd, canRemove o canApprove.
El permiso de edición efectivo normalmente combina autorización y contexto de pantalla:
readonly canEditForm = computed(() =>
this.authorization.can(this.currentUrl, 'EDIT') &&
(this.pageMode() === 'create' || this.pageMode() === 'edit'),
);
No sustituir esta regla por el modo solamente: estar en edit no significa que el usuario tenga autorización. Tampoco sustituir el permiso por deshabilitar el formulario, ya que puede haber acciones secundarias fuera de sus controles.
ActionGateAdapterLa librería @nebula-shared/data-access expone ActionGateAdapter y AuthorizationStore; el app.config.ts de nebula-shell registra NEBULA_ACTION_GATE usando ActionGateAdapter. Si una pantalla o componente del kit necesita integrar el gate, consumir el token/contrato público establecido por la app y mantener una sola fuente de decisión. No crear un gate paralelo ni asumir que registrar el provider valida automáticamente cada handler.
Consultar el uso real de ActionGateAdapter y AuthorizationStore en el repositorio antes de añadir una directiva nueva. La pantalla puede seguir usando AuthorizationStore.can() para reglas de vista y handlers si ese es el patrón ya adoptado por la feature.
Los triggers relacionan una acción de un recurso con un control de frontend mediante códigos. Para que la autorización visual funcione de manera coherente:
AuthorizationStore debe coincidir con el código configurado.Al agregar una nueva acción, acordar y configurar el código en Maestros, conectarlo al control pertinente y aplicarlo explícitamente en el frontend y backend.
AuthorizationStore desde @nebula-shared/data-access en el contenedor de la pantalla.libs/maestros/src/lib/feature-maestros/src/lib/rol/.libs/maestros/src/lib/feature-maestros/src/lib/acciones-triggers/.libs/maestros/src/lib/feature-maestros/src/lib/usuario/.libs/shared/data-access/src/lib/auth/authorization.store.ts y action-gate.adapter.ts.libs/shared/data-access/src/index.ts.apps/nebula-shell/src/app/app.config.ts.libs/invoicing/src/lib/feature-invoicing/src/lib/invoicing-concepts-revenue/invoicing-concepts-revenue.component.ts.AuthorizationStore implementadoEl store está en libs/shared/data-access/src/lib/auth/authorization.store.ts y se exporta públicamente desde @nebula-shared/data-access.
AuthorizationStore.load() obtiene roles[].id de localStorage.user_data y hace POST /simappe-admin/api/v1/menu-with-actions/by-roles con esos IDs. La respuesta contiene el menú con las acciones ya resueltas por Simappe. El frontend no combina roles ni calcula precedencia: indexa optionFullPath o optionPath y guarda el mapa actionCode -> boolean.
En el flujo de la aplicación, SidebarService.getMenu() invoca authorizationStore.load().subscribe() durante la carga del menú. Como el método es asíncrono y no se espera ahí, una pantalla inicial puede consultar permisos antes de que finalice la carga. Para evitar interpretar el estado inicial como una denegación definitiva, las pantallas pueden considerar loaded() al presentar/activar sus acciones, o el flujo de inicialización puede esperar a load() antes de navegar/renderizar contenido protegido.
Cuando el usuario inicia sesión o cambian sus roles, volver a llamar load() para actualizar el snapshot. El store no observa localStorage automáticamente.
private readonly authorization = inject(AuthorizationStore);
private readonly currentUrl = window.location.pathname;
readonly canEdit = computed(() =>
this.authorization.can(this.currentUrl, 'EDIT'),
);
can(url, actionCode) solo devuelve true si el mapa tiene literalmente true para el código. Si no encuentra la ruta o acción, devuelve false (fail closed). Los códigos, incluyendo mayúsculas/minúsculas, deben coincidir con lo devuelto por el API/configurado en Maestros.
actionsForRoute(url) expone el mapa efectivo para la opción. snapshot() es de solo lectura y loaded() indica que la carga —exitosa o vacía— terminó. Como can() lee la señal interna, invocarlo dentro de un computed crea la dependencia reactiva para recalcular tras la carga.
routeOptionSegment() elimina segmentos conocidos de acción (list, create, edit, view, detail, new, layout), IDs numéricos y UUIDs con el formato esperado; luego usa el último segmento restante. Por ejemplo:
/layout/accounting/tipo-plan/list → tipo-plan
/layout/accounting/tipo-plan/edit/42 → tipo-plan
El índice está agrupado solo por ese segmento. Por ello, los segmentos finales de opciones distintas deben ser únicos en el menú cargado. Si dos opciones de productos/módulos diferentes terminan en el mismo segmento, sus acciones pueden terminar combinadas en el mismo registro del mapa. Revisar las rutas configuradas en Maestros al crear módulos con slugs repetidos; si no se puede garantizar unicidad, el índice debería usar una clave más completa (por ejemplo, ruta normalizada) en vez de solo el último segmento.
Si el API falla, load() marca loaded=true y devuelve void; el mapa anterior no se limpia en esa rama de error. Si se recarga tras cambiar de usuario/roles, conviene limpiar las acciones al comenzar la carga o en catchError, para que un fallo no deje permisos del usuario anterior en memoria. La navegación protegida debería permanecer cerrada mientras la carga está incompleta o falló.
El AuthorizationStore decide si una acción está permitida, pero cada componente debe conectar el resultado con sus botones, transiciones y handlers. El provider NEBULA_ACTION_GATE registrado con ActionGateAdapter facilita la integración con controles que usan ese contrato, pero no reemplaza las validaciones explícitas de la pantalla ni la autorización del backend.