RBAC πΒΆ
This page describes the design of the Infinito.Nexus RBAC layer that provisions LDAP groups, synchronises them to Keycloak, and emits them as the OIDC groups claim.
For IAM fundamentals that span both providers, see common.md.
For the LDAP server layer, see ldap.md.
For the OIDC client layer, see oidc.md.
Operator-facing steps live in Administration RBAC.
LDAP layout contract π³ΒΆ
Every role that declares RBAC at the file root of its meta/rbac.yml MUST be provisioned with a per-application namespace under the container named by RBAC.GROUP.NAME (default roles). Each LDAP group entry is a direct child of that container; the application identifier (and the tenant identifier, if any) is encoded into the CN with hyphen separators:
cn=<application_id>-<role_name>,ou=roles,<LDAP_DN_BASE> # non-tenant / global role
cn=<application_id>-<tenant_id>-<role_name>,ou=roles,<LDAP_DN_BASE> # tenant-aware per-tenant
cn=<application_id>-<role_name>,ou=roles,<LDAP_DN_BASE> # tenant-aware scope=global
The producer build_ldap_role_entries also emits a per-application groupOfNames container cn=<application_id>,ou=roles,... whose member attribute references every role group of that application, so an LDAP-only consumer can still navigate the hierarchy. Keycloak does not rely on those containers; it reconstructs /roles/<application_id>/<role_name> via per-application mappers (see below).
rbac.tenancy schema πΒΆ
rbac:
tenancy:
axis: "domain" # one of: "none" (default), "domain"
source: "domains.canonical" # Jinja path, only "domains.canonical" is implemented
roles:
<role_name>:
description: "..."
scope: "per_tenant" # one of: "per_tenant" (default), "global"
An application SHOULD opt in to axis: domain only when its authorisation model is genuinely per-tenant and the tenant axis is the canonical domain. Typical consumer: WordPress Multisite. Applications whose authorisation is one global user set (Nextcloud, Gitea, Keycloak itself) MUST keep axis: none.
rbac_group_path lookup plugin π§ΒΆ
rbac_group_path is the only sanctioned way to derive the OIDC group path for an application role. Consumers MUST call it:
# non-tenant app
"{{ lookup('rbac_group_path', application_id='web-app-yourls', role='administrator') }}"
# -> "roles/web-app-yourls/administrator"
# tenant-aware, per-tenant role
"{{ lookup('rbac_group_path', application_id='web-app-wordpress',
role='editor', tenant='blog.example') }}"
# -> "roles/web-app-wordpress/blog.example/editor"
# tenant-aware, scope=global role
"{{ lookup('rbac_group_path', application_id='web-app-wordpress',
role='network-administrator') }}"
# -> "roles/web-app-wordpress/network-administrator"
Inline [RBAC.GROUP.NAME, ...] | path_join MUST NOT be used: it scatters the path shape across dozens of files and makes future layout changes partial. Centralising the path in the plugin guarantees that a layout evolution stays a one-file change.
Keycloak group tree and the groups client scope π«ΒΆ
The Keycloak group tree is built by per-application group-ldap-mappers. Each deployed application gets one mapper anchored at /roles/<application_id> whose LDAP filter (&(objectClass=groupOfNames)(cn=<application_id>-*)) surfaces only that applicationβs role groups. The mapper uses the LDAP description attribute as the visible Keycloak group name, so the resulting group paths carry no redundant prefix.
A shared mapper that imports every LDAP group with preserve.group.inheritance=true MUST NOT be used. Role names recur across applications (administrator, editor, β¦) and Keycloakβs GroupTreeResolver keys its lookup map on the group name, so the second administrator silently overwrites the first.
The groups client scope emits the OIDC groups claim with full.path=true, so consumers receive entries shaped like:
/roles/web-app-nextcloud/administrator
/roles/web-app-wordpress/<tenant>/<role>
/roles/web-app-wordpress/network-administrator
The scope MUST be a default client scope on every OIDC client that needs group-based RBAC.
Making it optional reintroduces a userinfo regression: when optional with include.in.token.scope=false, Keycloak drops the group-membership mapper from userinfo even when the scope is explicitly requested, so consumers receive no groups and fall back to subscriber.