RBAC
Una app declara qué roles pueden montarla. El shell declara quién es el usuario
actual. wu.mount() rechaza los que no encajan.
Lee esto primero: esta es una imposición del lado del cliente. Se ejecuta en
el navegador, en código que controla el usuario. Mejora la corrección y la
experiencia de uso —un módulo no autorizado nunca monta, nunca renderiza, nunca
emite— pero cualquiera con devtools puede llamar a
wu.setPrincipal({ permissions: ['mount:*'] }) y montar lo que le apetezca. La
barrera dura es que tu servidor se niegue a servir el bundle y se niegue a
atender las llamadas de API que hay detrás. El RBAC de aquí es el segundo
cerrojo, no el primero.
Declarar roles en una app
Sección titulada «Declarar roles en una app»Puede ser en la configuración de registro de la app, o en su manifiesto
wu.json bajo wu.roles (o un roles de primer nivel):
await wu.init({ apps: [ { name: 'catalog', url: '...' }, // público { name: 'admin', url: '...', roles: ['admin'] }, // restringido { name: 'reports', url: '...', roles: ['admin', 'analyst'] }, ],});{ "name": "admin", "wu": { "roles": ["admin"] }}Una app sin roles —o con un array vacío— es pública y monta siempre. La
configuración tiene prioridad sobre el manifiesto.
wu.setPrincipal()
Sección titulada «wu.setPrincipal()»setPrincipal(principal: { role?: string; roles?: string[]; permissions?: string[]; [key: string]: unknown;} | null): Principal | nullEstablece quién está usando el sistema. Devuelve el principal efectivo.
| Campo | Significado |
|---|---|
role |
Un único rol en forma de cadena. |
roles |
Roles adicionales. role y roles se respetan a la vez. |
permissions |
Concesiones explícitas. mount:* o mount:<appName> saltan la comprobación de roles. |
Cualquier otro campo que añadas viaja intacto — útil para un id de usuario o un nombre para mostrar que el shell quiera tener en un solo sitio.
// Después de que tu flujo de autenticación resuelvawu.setPrincipal({ id: 'u_1138', role: 'analyst', roles: ['reporting-beta'], permissions: ['mount:audit-log'],});
// Al cerrar sesiónwu.setPrincipal(null);Casos límite. Nunca lanza. Un argumento falsy se normaliza a null, lo que
significa que se deniegan todas las apps restringidas. El principal también se
refleja en el store en auth.principal, para que apps y devtools puedan leerlo
sin una referencia al framework — esa escritura es de mejor esfuerzo y se traga
el error si el store no está disponible. Emite principal:changed con
{ principal } en el bus de eventos, autenticado con un token interno para que
sobreviva al modo estricto.
Asignar un principal no desmonta las apps que ya están montadas. Solo afecta
a las llamadas posteriores a mount(). Si una bajada de privilegios de sesión
debe desmontar los módulos abiertos, hazlo tú:
wu.setPrincipal(lesserPrincipal);for (const name of wu.getStats().apps) { if (!wu.can(name)) await wu.unmount(name, { force: true });}wu.getPrincipal()
Sección titulada «wu.getPrincipal()»getPrincipal(): Principal | nullDevuelve el principal actual, o null si no hay ninguno.
const who = wu.getPrincipal();console.log(who?.role ?? 'anonymous');Casos límite. Devuelve el objeto vivo por referencia: mutarlo muta la copia
del framework sin emitir principal:changed. Llama de nuevo a setPrincipal()
en su lugar.
wu.can()
Sección titulada «wu.can()»can(appName: string): boolean¿Se le permitiría al principal actual montar esta app? Úsalo para decidir qué renderizar antes de intentarlo.
if (wu.can('admin')) { await wu.mount('admin', '#admin-slot');} else { renderAccessDenied();}La decisión, en orden:
- La app no declara roles →
true. Las apps públicas siempre pasan. - No hay principal →
false. permissionsincluyemount:*omount:<appName>→true.- Alguno de los roles del principal (de
rolemásroles[]) aparece en los roles requeridos por la app →true. - En cualquier otro caso →
false.
Casos límite. Nunca lanza, ni siquiera para un nombre de app que nunca se
registró: una app desconocida no tiene roles declarados, así que can()
devuelve true para ella. Es una consecuencia deliberada de “público por
defecto”, pero implica que can() no es una comprobación de registro. La
comparación de roles es una igualdad exacta de cadenas: no hay jerarquía, ni
herencia, ni comodines en los nombres de rol. Un rol admin no implica
analyst; enumera ambos.
Imposición en el montaje
Sección titulada «Imposición en el montaje»wu.mount() evalúa can() antes que cualquier otra cosa: antes del conteo
de referencias, antes del sandbox, antes de cualquier petición de red. Una
denegación es un lanzamiento limpio y temprano, sin efectos secundarios.
try { await wu.mount('admin', '#slot');} catch (err) { if (err.code === 'WU_ACCESS_DENIED') { console.warn(err.appName, 'requiere', err.required); renderFallback(); } else { throw err; }}El error lleva:
| Propiedad | Valor |
|---|---|
err.code |
'WU_ACCESS_DENIED' |
err.appName |
La app que fue rechazada |
err.required |
Los roles que declara |
err.message |
Legible por humanos, nombrando el rol actual y lo que se requería |
También se disparan dos notificaciones, para que un shell pueda renderizar un
fallback sin envolver cada montaje en un try:
// En el bus de eventoswu.on('access:denied', (e) => { const { appName, role, required } = e.data; showAccessDenied(appName, required);});
// En el DOM, para código sin referencia a wuwindow.addEventListener('wu:access:denied', (e) => { console.warn(e.detail);});Exports con nombre
Sección titulada «Exports con nombre»import { setPrincipal, getPrincipal, can } from 'wu-framework';
setPrincipal({ role: 'admin' });can('admin'); // trueLimitaciones honestas
Sección titulada «Limitaciones honestas»- Solo del lado del cliente. Se salta trivialmente desde una consola. Combínalo con imposición en el servidor: no sirvas un bundle al que el usuario no tiene derecho.
- Se impone al montar, no de forma continua. Cambiar el principal no desmonta las apps en marcha, y una app ya montada sigue funcionando.
- Solo
mount()está controlado.wu.update(), el store, el bus de eventos y los contratos de capacidad no tienen comprobaciones de rol. Una app denegada no puede montar, pero cualquier código de la página puede seguir leyendo el store y emitiendo eventos. - Roles planos. Comparación exacta de cadenas, sin jerarquía y sin comodines
en los nombres de rol (solo en
permissions, y solo para el verbomount:). - Las apps desconocidas están permitidas.
can('typo')devuelvetrue, porque una app sin roles declarados es pública. - El principal no es una credencial. Es una afirmación local sobre el
usuario. Nada lo verifica, y cualquier app de la página puede leerlo en
store.get('auth.principal')— así que no metas un token dentro.
