Tenants, users & scopes
Identity lives in Keycloak, not in lumnik. Creating a user, granting a role, and assigning a scope are console gestures; the hub follows the token.
lmhas no command for users, roles or tenants.
How lumnik identifies a user
lumnik trusts four claims from the access token:
| Claim | Purpose |
|---|---|
aud |
identifies the intended API — must contain lumnik-backend |
tenant_id |
which tenant the user belongs to |
groups |
the realm roles: lm_* open surfaces, scope:* limit data access |
user_id |
optional — binds the token to an existing hub row |
The bundled realm lumnik already maps the tenant_id and user_id user attributes onto those
claims. You set attributes and roles; nothing else.
- With a
user_id: the account is bound to that existing hub row. The bundledintegrator(user_id=1) has its row seeded at install. - Without a
user_id: depends on the edition, see Add a user.
For an external IdP, see Bring your own IdP and SCIM.
The console
http://localhost:8180, realm lumnik, admin credentials from your.env.- Brute-force detection is on: temporary lockout at Keycloak's defaults. Tune it under Realm settings → Security defenses.
- Local only. The compose pins Keycloak to
127.0.0.1, and the TLS proxy proxies login, tokens and the account page, never the admin console. On a remote host, use an SSH tunnel: reach the hub from another machine.
Helm (cloud edition): the bundled Keycloak keeps nothing
With auth.mode=oidc, the chart's Keycloak re-imports its realm at every pod start. Users,
roles, attributes and Enabled: off set in the console disappear at the next restart.
On Helm, manage users in your own IdP (auth.mode=oidc-external,
Bring your own IdP).
How to…
Add a user
- Users → Add user: username, email, Create.
- Credentials tab: set a password, or let your IdP federation handle it.
- Attributes tab:
tenant_id= the tenant's numeric id. - Role mapping tab → Assign role →
lm_user, plus anyscope:*role.
Edition
- lumnik Pro: nothing else to do. The first login creates the hub row (JIT provisioning).
- lumnik Open (no JIT): without a
user_idattribute the token is refused —401 {"error":"no external identity resolution in this edition (user_id claim required)"}. Complete the mapping yourself:- Create the hub row:
POST /api/tenants/{tid}/users, as anlm_adminof that tenant. - Note the
idit returns. - Add it as the account's
user_idattribute.
- Create the hub row:
POST /api/tenants/{tid}/users creates a hub row, not a login: nobody can authenticate as
it until a Keycloak account points at it.
Grant (or revoke) a business scope
Scope roles are created on demand. The shipped realm has none: zero scope:* roles means
unscoped, tenant-wide.
- Realm roles → Create role: name it
scope:ventes, the tag your manifests use inspec.scope. - Users → the user → Role mapping → Assign role →
scope:ventes.
To revoke, remove that role from the same Role mapping tab.
From their next token the user is bound:
- chat, search, views and entity reads answer only inside granted scopes; anything else is
403 {"error":"scope not granted"}; - the decision inbox lists only decisions in the granted scopes; an id from another scope is a
404; - an integrator administers only the connectors whose scopes they hold all of. What they see
Removing the last scope role widens access
A user with no scope:* roles has tenant-wide access. Revoking their last scope role
restores access to every scope in the tenant.
flowchart LR
u0["no scope:* role"] -->|"tenant-wide"| all
ua["scope:ventes"] --> V
ua -.->|"403 scope not granted"| S
subgraph all["one tenant's data"]
V[("scope: ventes")]
S[("scope: stock")]
end
Make someone admin, integrator, superadmin
Same Role mapping gesture. Roles are separate, not ranked: an integrator is not automatically an admin. What each one opens: Roles & permissions.
Onboard a new tenant
1. Create the tenant, as lm_superadmin (the bundled hubadmin account):
curl -s -X POST https://your-hub.example.com/api/tenants \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"code": "ACME", "name": "Acme Corp"}' # both required; note the returned id
$TOKEN is a hubadmin token: mint it as in Test the API,
with username=hubadmin and HUBADMIN_PASSWORD.
2. Create its first administrator: a Keycloak account as in Add a user, with
tenant_id = the id returned above and the roles lm_user + lm_admin.
| Edition | Then |
|---|---|
| lumnik Pro | Leave user_id unset. The first login creates the hub row in the new tenant. |
| lumnik Open | The account needs a hub row in the new tenant, and POST /api/tenants/{tid}/users answers 403 (empty body) to any token whose tenant_id is not {tid}, hubadmin included. |
On lumnik Open, that call passes only for a token holding lm_superadmin and lm_admin plus
an acting_as_tenant: <tid> claim (impersonation).
The bundled realm maps no such claim.
RLS fences every read and write of the new tenant's users to it from their first request.
Offboard someone
Archive the hub row, then disable the Keycloak account. The users API's
DELETE /api/tenants/{tid}/users/{id} (as lm_admin) archives; the next request is refused:
403 {"error":"user is deactivated"}. Why both, and how fast each acts:
The clock.