CodamAIDocs
Topicdone

User switch by header

How an authorized person works on behalf of another person, with their own roles or with those of the target person, and which checks come first.

Variants
with role, own roleswith role, roles of the target personwithout role → refusedtarget person unknown or not in the tenant → refusedlookup not possible → refusedinvalid value in user-roles → refusedwithout the target person's consent → refusedconsent for own roles, target requested → refusedconsent in another tenant → refusedconsent revoked → refused from the next requestservice account of an exempt clientconsent switched off (consent=off)request, approve, grant directly, revoke a consentwhat switches and what stayscreatehistorytogether with a tenant switchoperating mode SINGLE

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-rolesRoles, groups and attributes
missing or ownyour own
targetthose of the target person, as they would appear in their own token
any other valuethe request is refused

Case does not matter, so Target counts as target.

What switches and what stays

The RequestContext after the switch
Always switches
in both modes
  • 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 _userId on create: new rows belong to the target person
Always stays
in both modes
  • authenticated person: authenticatedUserId and authenticatedUserName stay yours
  • for CIAS itself you are the actor: administration, ceiling, audit
  • the request's tenant
Depends on the mode
own = yours, target = the target person's
  • 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

Is the user header applied?
  1. CIAS
    Tenant switch first
    If the request also carries tenant, that switch is applied first. The checks below then refer to the tenant the request ends up running under.
  2. CIAS
    Role
    Does the authenticated person have the realm role allowed-user-context-switch?
    ↳ no 403 cias.authentication.user-switch-denied
  3. CIAS
    Look up the target person
    Can CIAS find out who the target person is?
    ↳ no 403 cias.authentication.user-switch-unavailable
  4. CIAS
    Target person known?
    Is there a person with this ID?
    ↳ no 403 cias.authentication.user-switch-denied
  5. CIAS
    Member 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
  6. CIAS
    Consent of the target person
    Has 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, or user-switch-unavailable if the question cannot be answered
  7. 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 tenant attribute names the tenant.
  • Their allowedTenants attribute contains the tenant.

Every refusal looks the same:

A refused user switch
Request
POST /api/rest/note/query
Authorization: Bearer <token of Lena>
user: 3f2a…
Response
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.

What happens to the user header?
Realm role allowed-user-context-switchTarget personHeader user-rolesLookupConsent of the target personResult
––value other than own or target––403 user-switch-denied
no––––403 user-switch-denied
yes––not possible–403 user-switch-unavailable
yesunknown–possible–403 user-switch-denied
yesnot a member of the request's tenant–possible–403 user-switch-denied
yesmember, or SINGLE–possiblenone, expired, revoked, another tenant, or only own for target403 user-switch-not-consented
yesmember, or SINGLE–possiblecannot be checked403 user-switch-unavailable
yesmember, or SINGLEmissing or ownpossibleown or targetID and name of the target person, your roles and attributes
yesmember, or SINGLEtargetpossibletargetID, 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.

How the lookup is made

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.

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 consentMeaning
Grantorthe target person, always the authenticated person who gives it
Granteeexactly one person, never a role or a group
Tenantthe tenant in which the consent applies. None without tenants (SINGLE)
ModeOWN or TARGET. TARGET includes OWN
Enduntil when it applies. Without an end only if the installation allows it
Reasonoptional, 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.

Two ways to a consent

When: Support needs access, and the target person does not know about it yet.

  1. 1
    Client→CIAS
    Lena sends POST /cias/me/switch-requests with target, mode, validUntil and reason. For that she needs the role allowed-user-context-switch.
  2. 2
    CIAS
    creates the request and sends Ben the mail SWITCH_CONSENT_REQUESTED: who is asking, with whose permissions, until when, why
  3. 3
    Client→CIAS
    Ben 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_USED once.
  • 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-consents shows Ben what he consented to, with the first use. GET /cias/me/switch-consents/received shows Lena what she may do. Admins list a tenant at GET /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

SettingEnvironmentDefault
codamai.cias.user-switch.consentCIAS_USER_SWITCH_CONSENTrequired. off means: the role alone decides
codamai.cias.user-switch.consent-exempt-clientsCIAS_USER_SWITCH_CONSENT_EXEMPT_CLIENTSempty
codamai.cias.user-switch.max-consent-durationCIAS_USER_SWITCH_MAX_CONSENT_DURATIONno limit
codamai.cias.user-switch.allow-unlimited-consentCIAS_USER_SWITCH_ALLOW_UNLIMITED_CONSENTfalse, so every consent needs an end
codamai.cias.user-switch.request-expiryCIAS_USER_SWITCH_REQUEST_EXPIRYPT24H
codamai.cias.user-switch.revoker-rolesCIAS_USER_SWITCH_REVOKER_ROLEStenant-admin
codamai.cias.user-switch.consent-urlCIAS_USER_SWITCH_CONSENT_URLempty. 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

The variants of the user switch

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.

  1. 1
    Client→CDMS
    sends POST /note/query with header user: 3f2a…
  2. 2
    CIAS
    reads the token, determines Lena's tenant, roles and attributes
  3. 3
    CIAS
    role present, Ben known and a member → ID and name := Ben, roles and attributes stay Lena's
  4. 4
    CDMS→Database
    searches with _userId = 3f2a…

Result: The response contains Ben's notes, filtered with Lena's permissions.

When: As above, plus user-roles: target.

  1. 1
    Client→CDMS
    sends POST /note/query with user: 3f2a… and user-roles: target
  2. 2
    CIAS
    role present, Ben known and a member
  3. 3
    CIAS
    ID, name, realm roles, business roles, groups and attributes := Ben's
  4. 4
    CDMS→Database
    searches 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

Sources in the code and the knowledge base
  • CIAS/cias-authentication – JwtSessionFilter (headers user and user-roles), TokenParser.switchUser (order: tenant switch, then user switch), SwitchTargetLookup, RequestAdmission (USER_SWITCH_DENIED, USER_SWITCH_UNAVAILABLE)
  • CIAS/cias-kernel – SwitchTargetPort
  • CIAS/cias-user – LocalSwitchTargetAdapter, SwitchTargetLookupController (GET /cias/lookup/users/{sub}/identity), SwitchTargetLookupRoles (codamai.cias.user.switch-lookup-roles)
  • CIAS/cias-iam-api – RepresentedIdentityPort
  • CIAS/cias-iam-keycloak – KeycloakRepresentedIdentityAdapter (effective realm and client roles, organizations with roles, attributes with a mapper)
  • CIAS/cias-tenancy-client – RemoteSwitchTargetAdapter (codamai.cias.tenancy.lookup=remote, cache time codamai.cias.attribute-lookup.ttl)
  • CDMS/cdms-system-layer – AbstractLayer (set_userId on create, owner filter via getUserId)
  • CDMS/cdms-persistence-database – AuditRevisionEntity (acting_user_id, acting_username), AuditRevisionListener (userId, username, acting person from the RequestContext)
  • CDMS/cdms-commons – AuditRevisionMeta (actingUsername)
  • CIAS/cias-authentication – UserSwitchPolicy (codamai.cias.user-switch.consent, consent-exempt-clients), RequestAdmission.USER_SWITCH_NOT_CONSENTED
  • CIAS/cias-kernel – SwitchConsentQuery, SwitchConsentCheck
  • CIAS/cias-user – SwitchConsent, SwitchConsentService, SwitchConsentController (/cias/me/switch-consents, /cias/me/switch-requests, /cias/admin/switch-consents), GET /cias/lookup/users/{sub}/switch-consent, SwitchConsentPolicy
  • CIAS/cias-notification – templates SWITCH_CONSENT_REQUESTED, SWITCH_CONSENT_USED
Search