What this is about
Anna works for two tenants: nordbau and suedlogistik. In nordbau she is responsible for the region North, in suedlogistik for South. A single value region on the account cannot express that. Which value should it have?
That is why every attribute states at registration what it says something about:
| Binding | means | Examples | Where the value lives |
|---|---|---|---|
USER | one value per person, the same in every tenant | language, staff number, display name | on the account in Keycloak, arrives through the token |
USER_IN_TENANT | one value per person and tenant | regions, plants, cost centers | in CIAS, per person and tenant |
How an attribute becomes per tenant
The module says so at registration, see Registering attributes:
Attribute.optional("region").asMultivalued().perTenant()
Only the registering module knows what it means: CIAS sees that an attribute is called region, but not whether an installation gives a person different regions in two tenants. What CDMS generates from the model is always USER, see Two origins of attributes. A module writes an attribute per tenant into its registration itself.
You write the values via POST /cias/admin/users/{id}/tenant-attributes?tenantKey=…, checked by delegation and ceiling, see Who may write an attribute. This works only for attributes the catalog lists per tenant. CIAS refuses a USER attribute such as locale there, even for a platform administrator.
The scenario
-
1Admin→CIASwrites for Anna in
nordbau:region = [nord] -
2Admin→CIASwrites for Anna in
suedlogistik:region = [sued] -
3Client→CDMSAnna queries orders, tenant
nordbau -
4CIASchecks the token, settles the tenant, fetches Anna's values for
nordbau -
5CDMSfilters with
region IN (nord) -
6Client→CDMSAnna chooses
suedlogistikand asks again -
7CDMSfilters with
region IN (sued)Result: The same person, the same token, different rows per tenant
Before and after
flowchart LR
subgraph T["In Anna's token"]
TR["region: nord, sued<br/>(one value for the whole account)"]
TL["locale: de"]
end
subgraph C["In CIAS, tenant suedlogistik"]
CR["region: sued"]
end
subgraph E["Effective in suedlogistik"]
ER["region: sued"]
EL["locale: de"]
end
CR -- "replaces" --> ER
TL -- "unchanged" --> EL
TR -. "is replaced" .-> ER
The value from CIAS replaces the value from the token for this key. All other attributes, such as locale, come unchanged from the token. That holds even if CIAS does keep a row for such a key in the tenant: only what the application itself registers with perTenant() is replaced. If CIAS mixed them, the filter in one tenant would be wider than in either one alone, which is exactly what this prevents.
The variants
When: The attribute is registered without a binding or with USER.
CIAS takes the value from the token, the same in every tenant. There is no extra lookup.
Result: value from the token
When: The attribute is registered with perTenant().
On every request with a tenant, CIAS fetches the person's values in this tenant and inserts them.
Result: value from CIAS for the active tenant
When: The person belongs to exactly one tenant.
The same applies as with several: the value comes from CIAS for this one tenant. The difference from USER only shows once a second tenant is added.
Result: value from CIAS
When: The person is a member of several tenants and chooses one of them for the request.
The value comes for the chosen tenant. On a privileged switch into a tenant the person is not a member of, CIAS determines the values beforehand, for the starting tenant, and they keep applying in the target, like the roles. See Switch between tenants.
Result: a separate value per tenant
When: Fetching the values fails.
If CIAS has cached the values of this person in this tenant, those apply, even if they are older. Otherwise CIAS refuses the request. An outage should give nobody more reach and take from nobody what they were already working with.
Result: cached value, or 403 cias.authentication.tenant-not-served
Where the values come from
| Operating mode | What must be present | Lookup |
|---|---|---|
| embedded | cias-user with codamai.cias.user.persistence=jpa | method call in the same program |
| standalone | cias-tenancy-client in the module; in CIAS codamai.cias.user.lookup-rest=true and codamai.cias.user.lookup-roles with the role of the asking service | GET /cias/lookup/users/{sub}/attributes over HTTP |
| either | a module registers an attribute per tenant, but nothing can look up the values | the application does not start, the message names the attributes |
| standalone | endpoint in CIAS not switched on or role missing | every request with a tenant is refused: 403 cias.authentication.tenant-not-served |
CIAS caches the answer per person and tenant for 30 seconds, configurable with codamai.cias.attribute-lookup.ttl. The number of cached pairs is limited by codamai.cias.attribute-lookup.max-entries. So the lookup costs at most one call per person, tenant and half minute. An installation without an attribute per tenant never asks.
A request without a tenant, for example in operating mode SINGLE, has no values per tenant. There the value from the token applies.