What this is about
On every request CDMS has to know which tenant it runs for. Only then does it find the right database. The tenant comes from the token that Keycloak issued and signed. The client does not send it as a separate field, and CDMS does not take it from any field in the data either.
The CIAS filter chain reads the token before CDMS does anything. It stores the result in the RequestContext. That is a store that lives for exactly one request and is cleared at the end. Everything later reads the tenant only from there.
The path of the tenant
flowchart LR
T["Token<br/>organization: acme<br/>tenant: acme<br/>allowedTenants: …"] --> F["CIAS filter chain<br/>reads and checks"]
H["Header tenant<br/>(only a wish)"] -.-> F
F --> RC["RequestContext<br/>tenant: acme<br/>allowed tenants: acme, …"]
RC --> P["CDMS persistence<br/>chooses the database"]
P --> DB[("Database acme")]
Where the tenant is in the token
Keycloak can write the tenant into the token in two ways:
- Organization: The person is a member of an organization in Keycloak. The token carries it in the
organizationclaim. The alias of the organization, its short name in Keycloak, is the tenant key. This is how tenants come about that CIAS creates at runtime. - Attribute
tenant: The person has a user attributetenantholding the tenant key. That is the permanently assigned tenant.
A tenant key consists only of lowercase letters, digits and hyphens, for example acme or stadtwerke-nord. It is also the name of the tenant’s database.
How tenants and organizations come about in CIAS is explained in Static and dynamic tenants.
Which tenant applies
| Organizations in the token | Attribute tenant | Header tenant names an own organization | Tenant of the request |
|---|---|---|---|
| none | missing | – | no tenant |
| none | acme | – | acme |
| one or more | names none of them | – | 403 cias.authentication.tenant-unresolved: the two sources contradict each other |
| several | – | yes, e.g. globex | globex: selection among the person's own organizations |
| several | names one of them | no | the organization from the attribute |
| exactly one | missing | no | that one organization |
| several | missing | no | 403 cias.authentication.tenant-unresolved: unclear whose data is meant |
Read the rows from top to bottom. There is no default tenant CIAS falls back to. A wrong tenant would be worse than no answer: it would show one customer the data of another.
What ends up in the RequestContext
Once the tenant is determined, CIAS also checks whether it is served. Then it writes into the RequestContext:
| Entry | Content |
|---|---|
| Tenant | the tenant key just determined |
| Allowed tenants | the own tenant, all own organizations and all entries from the user attribute allowedTenants |
| Switch wish | the value of the tenant header, if present |
| User, roles, attributes | as described in The three levels of security |
The list of allowed tenants limits where a tenant switch can lead at all. It comes from the signed token, never from a header. The attribute allowedTenants may have several values or be a comma-separated list.
Variants
When: MULTI, the token names exactly one tenant
-
1Client→CDMSsends the request with
Authorization: Bearer … -
2CIASchecks the token and determines the tenant
acme -
3CIASasks whether
acmeis served: yes -
4CDMS→Databasereads and writes tenant models in the database
acme
Result: The request runs entirely in the tenant acme.
When: The token belongs to a real person or a service account but names neither an organization nor the attribute tenant.
-
1CIASchecks the token: valid, but without a tenant
-
2CIAS→Client403
cias.authentication.tenant-required, before CDMS even sees the request
Result: The error says: this person was never assigned a tenant. That is fixed in Keycloak or CIAS, not in the client.
When: A request still reaches the persistence without a tenant, for example from custom code.
-
1CDMSwants to read or write a tenant model and finds no tenant
-
2CDMS400
CDMS_TENANT_REQUIRED, no fallback to the system database
Result: System models are not affected; they need no tenant.
When: CODAMAI_PERSISTENCE_TENANT_MODE=SINGLE
CIAS does not evaluate the organization or the attribute tenant at all. The request runs without a tenant, the list of allowed tenants stays empty, a tenant header has no effect. Everything ends up in the one database.
Result: A token with a tenant and one without behave the same in SINGLE.
The error responses of the filter chain
When the filter chain refuses, the response comes from CIAS and not from CDMS. It therefore has its own short format:
POST /api/rest/crm/customer/query
Authorization: Bearer <token with two organizations, no selection>HTTP 403
{ "error": "cias.authentication.tenant-unresolved", "message": "request refused" }| Key | Meaning |
|---|---|
cias.authentication.tenant-unresolved | The token names tenants, but not exactly one. |
cias.authentication.tenant-required | The token names no tenant at all, although the installation is MULTI. |
cias.authentication.tenant-not-served | The tenant is unambiguous but is not served. See Is the tenant served? |
An invalid or expired token is a different case: that results in 401, see Access without a token.
Pitfalls
Where to go next
- What happens to the tenant in the persistence: Which database? The persistence target
- Working for another tenant: Tenant switch by header
- The CIAS view: Determining the tenant of a request
- What a tenant key looks like: The tenant key