What this is about
CIAS either runs in the same process as CDMS or as a separate service. That decision is a decision about operations: about processes, the network and outages. It must not be a decision about who is allowed to do what.
The reason is simple: otherwise there would be two systems under one name. A customer who is served in one and refused in the other would not be an operational difference but a security incident — and you would notice only when somebody moved.
How this is achieved technically
Between CDMS and CIAS there is no direct call but a port. A port is an interface: a question as a method signature, with no statement about who answers it or how. The answer comes from an adapter, and there are exactly two of them — one per topology.
flowchart TB
G["Caller in cias-authentication<br/>TenantGate / AttributeLookup<br/><i>rules, cache, refusal</i>"]
G --> P{{"Port<br/>TenantLookupPort"}}
P -->|embedded| LA["LocalTenantLookupAdapter<br/>(cias-tenancy)"]
P -->|standalone| RA["RemoteTenantLookupAdapter<br/>(cias-tenancy-client)"]
RA -->|HTTP| CTL["TenantLookupController<br/>in the CIAS service"]
LA --> UC["TenantManagementUseCase.standing"]
CTL --> UC
UC --> DB[("CIAS tenants")]
Read the picture from the bottom: both paths end at the same method. The local adapter calls it directly. The remote adapter sends HTTP to an endpoint in the CIAS service, and that endpoint calls exactly the same method. So the rule about whether a tenant may be served exists only once.
The same holds at the top: the caller is the same class in both topologies. The tenant gate’s cache, the 30-second window, refusing when in doubt — all of that belongs to the caller, not to the adapter. The adapters themselves must neither cache nor retry nor soften an answer.
The three questions across the boundary
Only three questions travel between CDMS and CIAS. Each has a port and two adapters:
| Question | Who asks | Embedded | Standalone |
|---|---|---|---|
| May tenant X be served right now? | tenant gate, on every request | method call in cias-tenancy | GET /cias/lookup/tenants/{key} |
| Which attribute values does this person hold in this tenant? | filter chain, on every request with a tenant | method call in cias-user | GET /cias/lookup/users/{id}/attributes |
| Which roles and attributes does this module register? | CIAS, at start and on demand | call of a bean in the same process | GET /cias/fetch on the module |
For the first two, CDMS asks CIAS. For the third it is the other way round: there CIAS asks CDMS. More on that under Modules register roles and attributes.
How the two are kept from drifting apart
Two adapters, written at different times, drift apart by themselves. Against that, every port has a shared contract suite: a set of tests that does not check one adapter but the port. Every adapter signs up to the same suite and has to pass it.
What these suites pin down is always the same kind of distinction — and it is the real reason they exist:
-
1CIASserved — the tenant exists and may workThe request carries on.
-
2CIASexists, but not served — suspended, closed, or outside its windowRefused. The log says that somebody was deliberately suspended.
-
3CIASnobody has heard of it — no tenant carries that keyRefused as well, but for a different reason. For an operator this is a completely different next step.
-
4CIAScould not be asked — timeout, refused connection, unreadable answerResult: This is not a "no". An adapter that turned it into one would report every fault as a deliberate suspension. It throws instead, and the caller decides — out of its cache or with a refusal.
The same care applies to the third question: “registers nothing” and “could not be asked” must not be confused. The role reconciliation retires what a module no longer registers — an adapter that passed an outage through as an empty registration would strip a service of its roles just because it happened to be restarting.
Where the difference does show
“Functionally identical” does not mean “identical in every respect”. It means: what is decided is the same. What is not the same is what an outage means and how long something takes.
- which tenant is served and which is not
- which roles and attribute values apply in a request
- which requests are refused, and with which error key
- what a role grant may do and what the ceiling forbids
- which roles the reconciliation creates, renames or marks deprecated
- latency: a method call against an HTTP call over the network
- outage: only standalone can CIAS fail on its own
- credentials: only standalone does CDMS need a service account of its own
- start-up checks: the standalone service additionally checks for leftover development settings
- databases: one shared against two separate ones
Three of these deserve a closer look.
The outage. Embedded there is no “half”: either the process runs or it does not. Standalone, CIAS can be down while CDMS keeps running. Then the tenant gate’s cache decides, and it decides by a deliberately asymmetric rule: an outage may not evict anyone who was already working, and may not admit anyone who was not. Whoever is in the cache carries on with the last known answer — a refusal included. Whoever is not in it is refused. Details under When CIAS is not reachable.
The delay of a suspension. The tenant gate remembers its answer for a short while, 30 seconds by default. So when you suspend a tenant, it takes effect after that window at the latest — in both topologies. Embedded this is easy to underestimate, because you assume that in the same process it must be immediate. But the window belongs to the caller, and the caller exists in both topologies.
The start-up checks. Both operating modes check at start whether development settings have been left in place, and then do not start unless codamai.safety.mode is warn. The one difference is the schema: standalone CIAS additionally requires ddl-auto: validate; embedded, the schema belongs to the application — see Reject when in doubt.
Where it does not show
For the client calling the API, the topology is invisible. There is no header, no field in the response and no error key you could read it from. A refused request looks the same in both topologies, and that is deliberate: refusals must not reveal anything about the internal structure. See Rejections that reveal nothing.
Roles and permissions do not shift invisibly either. What a role allows on the data is in the model and is enforced by CDMS — and CIAS appears there in neither topology.