Role management¶
Operator guide for the lumnik RBAC catalog.
Conceptual model¶
Lumnik distinguishes two layers of authorisation metadata:
- Realm roles — declared in Keycloak, attached to user accounts. Currently four:
lm_user(read business surfaces, e.g./api/entities/*),lm_integrator(drive connectors and ingestion),lm_admin(administer a tenant),lm_superadmin(platform-wide: tenant list + impersonation).
No hierarchy. The interceptor matches role names flat —
lm_admindoes not implylm_user. Grant every role a user needs explicitly (the self-host demo realm does:integratorcarries integrator+admin+user,hubadminadmin+user+superadmin; the native-claims k8s variant grantshubadminonlylm_admin). - Permissions — declared per module inMETA-INF/module-manifest.yaml(permissions:list). Examples:repair.read,repair.manage,request.manage.Métier-scope grants are realm roles too, by naming convention:
scope:<tag>(e.g.scope:achat). They are opt-in — a user with none is unscoped (tenant-wide RAG access); a user with one or more may only query those scopes. No entry inroles.yamlis needed; see Scopes.
The mapping between the two — which role grants which permission — lives in
lumnik-hub/src/main/resources/META-INF/roles.yaml and is exposed in the admin
console under Rôles (/admin/{tid}/roles).
Current enforcement model:
@RequiresPermission(...)annotations on REST resources still reference realm roles as magic strings (e.g.@RequiresPermission("lm_admin")). The permission catalog shipped here is declarative metadata — it documents the mapping but does not (yet) drive the interceptor. A future plan will refactor the interceptor to resolve permissions throughRoleRegistry.
Adding a new role¶
- Edit
lumnik-hub/src/main/resources/META-INF/roles.yamland add the role underroles:. - Declare the role in the Keycloak realm
lumnik(or your environment's equivalent). - Restart the platform. Boot logs should show
Roles loaded: N roles from M roles.yaml files. - Visit
/admin/{tid}/rolesand confirm the new role appears.
If the platform refuses to start with a message like
Role 'lm_foo' references unknown permission 'bar.baz', either remove the permission from
the role grant or add it to the relevant module's module-manifest.yaml.
Adding a new permission¶
- Add the permission id to the relevant
module-manifest.yaml:permissions: - id: invoice.export label: en: "Export invoices" fr: "Exporter les factures" - Grant the permission to the roles that should have it by adding the id under
roles.yaml→roles[].permissions. - Restart the platform. If the permission is not yet granted by any role, you will see a
WARN log:
Orphan permissions (declared by modules, granted by no role): [invoice.export]. - Use the permission id in a
@RequiresPermissionannotation. Today, until the interceptor refactor lands, this still resolves against realm roles — so the annotation value must remain the realm role name (lm_adminetc.). The permission catalog is only metadata at this stage.
Dev-bypass¶
Setting lumnik.security.dev-bypass=true in application.properties (or via the env var
LUMNIK_SECURITY_DEV_BYPASS=true) injects the three realm roles
{lm_superadmin, lm_admin, lm_user} into every request. This is enabled by default in
%dev and %test profiles.
To test access refusals locally, set the flag to false and provide a valid JWT with the
required roles in the groups claim. There is no header that overrides roles — dev-bypass
grants the fixed set above (identity comes from X-Tenant-ID / X-User-ID headers); in
production the roles always come from the JWT.
The entity-read scope gate¶
Realm roles answer may this user call the endpoint; scope grants answer which métier's
data may they see there. A scope-bound user (one or more scope:<tag> realm roles)
hitting a read surface outside their granted set gets the uniform refusal:
403 {"error":"scope not granted"}
The check is ScopeGrants.allowsAny(...) (lumnik-common) against the scope tags on the
resource — e.g. EntityReadResource refuses GET /api/entities/{name} when the entity
manifest's scopes don't intersect the user's grants. The same gate covers saved-view
discovery, view render/detail/generate, and chat. Unscoped users (no scope:* role) pass
everything — the boundary is opt-in. Full semantics:
Scopes → Binding users to scopes.
Release security checklist¶
Before each release, walk through the catalog at /admin/{tid}/roles and confirm:
- The list of roles matches what is declared in Keycloak.
- Only
lm_superadmincarries thewildcard: trueflag. - No permission is declared by a module without being granted by at least one role (the boot log will warn — investigate each orphan).
- Every
@RequiresPermissionannotation in the codebase uses a role id present inroles.yaml:grep -rn '@RequiresPermission' lumnik-*/src/main/java
Audit logging¶
PermissionInterceptor (in io.lumnik.platform.security) logs every access denial at
WARN level with the required roles, the caller's roles, and the method name. SIEM consumers
should subscribe to these log lines for security monitoring. The interceptor does not
currently write to a structured audit table — that is on the roadmap.
See also¶
- The role → permission mapping:
lumnik-hub/src/main/resources/META-INF/roles.yaml - The bundled demo realm (roles + users):
infra/keycloak/lumnik-realm.json - The database layer beneath RBAC: RLS & database roles