What this is about
In user models every person sees only their own rows. Sometimes, though, someone has to see exactly what a particular other person sees, for example support staff tracking down an error. That is what the user switch is for: the client sends the user header with the ID of the other person, the target person.
The ID of a person is their value in the sub claim of the token, i.e. their Keycloak ID, a long identifier like 3f2a…. The login name does not work.
With a second, optional header you choose whose permissions you work with:
Header user-roles | Roles, groups and attributes |
|---|---|
missing or own | your own |
target | those of the target person, as they would appear in their own token |
| any other value | the request is refused |
Case does not matter, so Target counts as target.
What switches and what stays
- the person's ID (
userId) - the person's name (
userName) - and with it the owner filter: you see the target person's rows
- and with it
_userIdon create: new rows belong to the target person
- authenticated person:
authenticatedUserIdandauthenticatedUserNamestay yours - for CIAS itself you are the actor: administration, ceiling, audit
- the request's tenant
- realm roles
- business roles (client roles)
- groups
- attributes, and with them every attribute filter
With target the takeover is complete. You also get roles you do not have yourself, even platform roles of the target person. The same rules apply as for a real token of the target person:
- If they are a member of the tenant’s organization and the membership carries roles, these replace their global business roles. See Effective roles.
- Values registered per tenant are fetched by CIAS for the target person, not for you. See One value per person or per tenant.
The checks in order
-
CIASTenant switch firstIf the request also carries
tenant, that switch is applied first. The checks below then refer to the tenant the request ends up running under. -
CIASRoleDoes the authenticated person have the realm role
allowed-user-context-switch?↳ no 403cias.authentication.user-switch-denied -
CIASLook up the target personCan CIAS find out who the target person is?↳ no 403
cias.authentication.user-switch-unavailable -
CIASTarget person known?Is there a person with this ID?↳ no 403
cias.authentication.user-switch-denied -
CIASMember of the tenant?Only if the request runs under a tenant: does the target person belong to it?↳ no 403
cias.authentication.user-switch-denied -
CIASConsent of the target personHas the target person consented to you switching, for this tenant and this mode, and is the consent still valid? Asked anew on every request.↳ no 403
cias.authentication.user-switch-not-consented, oruser-switch-unavailableif the question cannot be answered - ID and name of the target person in the RequestContext, roles depending on the mode
A user-roles header with a value other than own or target also leads to 403 cias.authentication.user-switch-denied.
The target person is a member if one of these holds:
- They are a member of the organization with the tenant’s alias.
- Their
tenantattribute names the tenant. - Their
allowedTenantsattribute contains the tenant.
Every refusal looks the same:
POST /api/rest/note/query
Authorization: Bearer <token of Lena>
user: 3f2a…HTTP 403
{ "error": "cias.authentication.user-switch-denied",
"message": "request refused" }Why exactly it was refused, i.e. role, unknown or not in the tenant, is only in the log. That way nobody learns through the header which IDs exist.
Only a missing consent has a key of its own, cias.authentication.user-switch-not-consented. It is checked only once it is established that the target person exists and belongs to the tenant. A client can react to it and offer to request a consent.
| Realm role allowed-user-context-switch | Target person | Header user-roles | Lookup | Consent of the target person | Result |
|---|---|---|---|---|---|
| – | – | value other than own or target | – | – | 403 user-switch-denied |
| no | – | – | – | – | 403 user-switch-denied |
| yes | – | – | not possible | – | 403 user-switch-unavailable |
| yes | unknown | – | possible | – | 403 user-switch-denied |
| yes | not a member of the request's tenant | – | possible | – | 403 user-switch-denied |
| yes | member, or SINGLE | – | possible | none, expired, revoked, another tenant, or only own for target | 403 user-switch-not-consented |
| yes | member, or SINGLE | – | possible | cannot be checked | 403 user-switch-unavailable |
| yes | member, or SINGLE | missing or own | possible | own or target | ID and name of the target person, your roles and attributes |
| yes | member, or SINGLE | target | possible | target | ID, name, roles, groups and attributes of the target person |
How CIAS knows the target person
The token only contains the authenticated person. So CIAS asks about the target person and recomputes from Keycloak what their token would contain:
- their effective realm roles, including those from composite roles and default roles,
- their effective client roles on the backend client,
- their organization memberships with the roles of their organization groups,
- their user attributes for which the client has an attribute mapper, the way CIAS creates them.
Not reproduced are mappers on other client scopes and a groups claim. When in doubt, with target you see less than the target person’s real token, never more.
When: CIAS with its user management runs in the same program as CDMS, for example in the hub.
The lookup is a plain method call.
Result: Nothing to set up.
When: CDMS and CIAS run as separate services.
CDMS asks GET /cias/lookup/users/{sub}/identity?client=<backend-client>, with the same service token as for the tenant lookup. In CDMS this needs codamai.cias.tenancy.lookup=remote. In CIAS, codamai.cias.user.lookup-rest=true must be set (the same switch as for the attribute lookup), and the dedicated role list codamai.cias.user.switch-lookup-roles (environment CIAS_USER_SWITCH_LOOKUP_ROLES) must contain the service token's role.
Result: An empty role list means nobody may ask, and every switch ends with user-switch-unavailable.
The role list is deliberately not the one of the attribute lookup. The answer contains all roles and attributes of a person, which reveals more than a single attribute value.
CDMS remembers an answer per person and client for 30 seconds (codamai.cias.attribute-lookup.ttl). If CIAS cannot be reached after that, CDMS does not fall back to an older answer: the switch is refused.
The target person’s consent
A consent is a person’s agreement that one particular other person may work on their behalf. It is bound to everything that makes up the switch:
| Part of the consent | Meaning |
|---|---|
| Grantor | the target person, always the authenticated person who gives it |
| Grantee | exactly one person, never a role or a group |
| Tenant | the tenant in which the consent applies. None without tenants (SINGLE) |
| Mode | OWN or TARGET. TARGET includes OWN |
| End | until when it applies. Without an end only if the installation allows it |
| Reason | optional, for example a ticket number. Appears in the mail and in the audit |
A consent for OWN does not cover user-roles: target. A consent in acme does not cover initech.
How a consent comes about
When: Support needs access, and the target person does not know about it yet.
-
1Client→CIASLena sends
POST /cias/me/switch-requestswithtarget,mode,validUntilandreason. For that she needs the roleallowed-user-context-switch. -
2CIAScreates the request and sends Ben the mail
SWITCH_CONSENT_REQUESTED: who is asking, with whose permissions, until when, why -
3Client→CIASBen approves with
POST /cias/me/switch-consents/{id}/approve, optionally with an earlier end of his own. Or he declines with.../decline.
Result: The request becomes the consent. A second identical request returns the open one, without a second mail. An unanswered request lapses after CIAS_USER_SWITCH_REQUEST_EXPIRY (24 hours by default).
When: Ben wants Lena to handle his cases this week, on his own initiative.
Ben sends POST /cias/me/switch-consents with grantee, mode, validUntil and reason. The grantor is always Ben himself; there is no field with which he could enter somebody else.
Result: The consent applies at once.
What happens afterwards:
- First use: on the first switch that rests on the consent, Ben receives the mail
SWITCH_CONSENT_USEDonce. - Revocation:
DELETE /cias/me/switch-consents/{id}. It takes effect with the next request, because the consent is never cached. Ben, Lena, a platform admin and a tenant admin (tenant-admin) in their own tenant may revoke. Only Ben may grant. - Overview:
GET /cias/me/switch-consentsshows Ben what he consented to, with the first use.GET /cias/me/switch-consents/receivedshows Lena what she may do. Admins list a tenant atGET /cias/admin/switch-consents. - Audit: request, consent, decline, revocation and first use appear as
SwitchConsentEvent.*in the CIAS trail.
For a consent you may not see, CIAS answers 404, as if it did not exist.
What an installation sets
| Setting | Environment | Default |
|---|---|---|
codamai.cias.user-switch.consent | CIAS_USER_SWITCH_CONSENT | required. off means: the role alone decides |
codamai.cias.user-switch.consent-exempt-clients | CIAS_USER_SWITCH_CONSENT_EXEMPT_CLIENTS | empty |
codamai.cias.user-switch.max-consent-duration | CIAS_USER_SWITCH_MAX_CONSENT_DURATION | no limit |
codamai.cias.user-switch.allow-unlimited-consent | CIAS_USER_SWITCH_ALLOW_UNLIMITED_CONSENT | false, so every consent needs an end |
codamai.cias.user-switch.request-expiry | CIAS_USER_SWITCH_REQUEST_EXPIRY | PT24H |
codamai.cias.user-switch.revoker-roles | CIAS_USER_SWITCH_REVOKER_ROLES | tenant-admin |
codamai.cias.user-switch.consent-url | CIAS_USER_SWITCH_CONSENT_URL | empty. Otherwise the address appears in the mails |
Every setting also works directly as an environment variable, in every program that embeds CIAS. An unknown value for consent prevents the start. An end in the past, beyond the maximum duration, or a missing end without permission is refused by CIAS with 422 cias.user.switch-consent-invalid.
Run separately, CDMS asks for the consent at GET /cias/lookup/users/{sub}/switch-consent?actor=…&tenantKey=…&mode=own|target, with the same service token and the same roles as for looking up the target person. Unlike the identity, the answer is never remembered.
Variants
When: Lena has allowed-user-context-switch and sends user: 3f2a…, without user-roles. Ben, with the ID 3f2a…, is a member of the tenant.
-
1Client→CDMSsends
POST /note/querywith headeruser: 3f2a… -
2CIASreads the token, determines Lena's tenant, roles and attributes
-
3CIASrole present, Ben known and a member → ID and name := Ben, roles and attributes stay Lena's
-
4CDMS→Databasesearches with
_userId = 3f2a…
Result: The response contains Ben's notes, filtered with Lena's permissions.
When: As above, plus user-roles: target.
-
1Client→CDMSsends
POST /note/querywithuser: 3f2a…anduser-roles: target -
2CIASrole present, Ben known and a member
-
3CIASID, name, realm roles, business roles, groups and attributes := Ben's
-
4CDMS→Databasesearches with
_userId = 3f2a…, checks Ben's roles, filters with Ben's attributes
Result: Lena sees what Ben sees, as far as Keycloak lets Ben's token be reproduced.
When: Lena lacks allowed-user-context-switch.
The header is not skipped; the whole request is refused.
Result: 403 cias.authentication.user-switch-denied.
When: There is no person with the ID, or they do not belong to the tenant the request runs under.
Both end the same way. A typo in the ID therefore does not lead to an empty response but to a refusal.
Result: 403 cias.authentication.user-switch-denied.
When: CIAS cannot be reached, refuses access, or the lookup is not set up.
Without knowing who the target person is, CIAS does not switch. An older answer is not used.
Result: 403 cias.authentication.user-switch-unavailable.
When: The request sends, for example, user-roles: admin.
An unknown value is not read as own.
Result: 403 cias.authentication.user-switch-denied.
When: Lena has the role, Ben is a member, but Ben has not consented to Lena.
Role, target person and membership are fine. Then CIAS asks for the consent and finds none. Nothing is switched.
Result: 403 cias.authentication.user-switch-not-consented. Lena can submit a request.
When: Ben gave Lena OWN, Lena sends user-roles: target.
OWN does not cover target. The same request without user-roles goes through.
Result: 403 cias.authentication.user-switch-not-consented.
When: Ben's consent applies to acme, Lena works under initech.
A consent applies in exactly one tenant.
Result: 403 cias.authentication.user-switch-not-consented.
When: Ben revokes while Lena is working, or the end is reached.
The consent is checked anew on every request. The very next request ends with the refusal.
Result: 403 cias.authentication.user-switch-not-consented.
When: A technical client, for example an import, is listed in CIAS_USER_SWITCH_CONSENT_EXEMPT_CLIENTS and switches with its own service account token.
A service account cannot ask anybody for consent. CIAS recognizes the token by azp being the client and preferred_username reading service-account-<client>. A person signing in through the same client is not exempt.
Result: The switch needs no consent. Role, target person and membership are still checked.
When: CIAS_USER_SWITCH_CONSENT=off
The installation deliberately decides that the role alone is enough. A warning appears in the log at startup.
Result: The switch works without consent; the other checks remain.
When: With an effective switch, an object of a user model is created, in either mode.
CDMS sets _userId to the target person's ID. The new object belongs to them, and without the switch you no longer see it.
Result: This is how data can be created for another person. Sending _userId in the JSON does not achieve that.
When: With an effective switch, an audited model is changed.
The revision names the target person as user ID and name. In addition it records the ID and name of the authenticated person as the acting person. In the history, their name appears in actingUsername.
Result: See What a revision records.
When: The request carries the headers tenant and user.
Each switch needs its own realm role. The tenant is switched first, following the rules in Tenant switch by header. Then the person is switched, and the target person's membership is checked for the target tenant.
Result: You work as the target person in the other tenant, with your roles or with theirs.
When: CODAMAI_PERSISTENCE_TENANT_MODE=SINGLE
There is no tenant, so the membership check is skipped. Role, lookup and a known target person are still checked.
Result: The owner filter exists in both operating modes, and so does the switch.
Pitfalls
Where to go next
- Why user models show only own rows: Only your own data (owner filter)
- Working for another tenant: Tenant switch by header
- What a revision records: What a revision records
- An example from start to finish: Support looks into a tenant