CodamAIDocs
Topicdone

Admit the tenant (tenant gate)

Resolving and admitting are two steps. How the gate asks, remembers for 30 seconds, answers from memory during an outage, and why unknown and suspended look the same.

Variants
CIAS reachableCIAS down, tenant knownCIAS down, tenant unknownsuspended tenantsuspension takes effect up to one TTL later

What this is about

Once the tenant of a request is resolved, it is clear which tenant is meant. Whether this tenant may work today is a second question. A customer can be suspended, their contract can have expired, or they do not exist at all.

The tenant gate asks this second question. It sits in the filter chain directly after the resolution, so before every module. The answer always comes from CIAS, because only CIAS manages the tenants.

The path through the filter chain

From the token to the admitted tenant
  1. Filter chain
    Check the token
    Signature and expiry valid?
    ↳ no 401
  2. Filter chain
    Resolve
    exactly one tenant, or deliberately none?
    ↳ no 403 cias.authentication.tenant-unresolved
  3. Tenant gate
    Admit
    Is this tenant served today?
    ↳ no 403 cias.authentication.tenant-not-served
  4. Filter chain
    Switch
    Did the header tenant switch the tenant? Then ask the gate once more for the target
    ↳ no 403 cias.authentication.tenant-not-served
  5. RequestContext with an admitted tenant

If a request has no tenant, the gate has nothing to ask and lets it pass. Whether a request without a tenant is allowed is decided by another rule, see The four results.

What “served” means

CIAS answers yes if the tenant has the standing ACTIVE and today’s date is within its validity window. All other cases are no. The details are in The lifecycle of a tenant.

Where the gate gets the answer

The gate asks through an interface, the TenantLookupPort. How the question reaches CIAS depends on whether CIAS runs in the same process:

sequenceDiagram
    participant F as Filter chain
    participant G as Tenant gate
    participant L as CIAS in the same process
    participant R as CIAS as a separate service
    F->>G: Is nordbau served?
    alt embedded (lookup: local)
        G->>L: Method call
        L-->>G: served: yes
    else standalone (lookup: remote)
        G->>R: GET /cias/lookup/tenants/nordbau (service token)
        R-->>G: 200 {"key":"nordbau","served":true}
    end
    G-->>F: admitted
embeddedstandalone
Setting on the asking sidecodamai.cias.tenancy.lookup=localcodamai.cias.tenancy.lookup=remote plus codamai.cias.tenancy.client.base-url
Pathmethod callHTTP request with a token of the service
If the tenant does not existempty answer404
Waiting timenoneat most 2 seconds to connect and 2 seconds for the answer, then CIAS counts as unreachable
Setting on CIAS–codamai.cias.tenancy.lookup-rest=true and lookup-roles: which roles may ask

lookup has no default. If the setting is missing, the application does not start. This is on purpose: a gate that lets everything pass without someone to answer would be a gap that a forgotten entry opens.

The endpoint for the standalone mode reveals only two things: the key and whether it is served. No display name, no data, no standing. It has its own roles, separate from the administration API. Otherwise every CDMS node would have a token it could use to close customers, just to ask a yes-no question.

The gate remembers answers

The question comes with every request, the answer rarely changes. So the gate remembers every answer, every no as well, for a short time: by default 30 seconds (codamai.cias.tenant-gate.ttl). It remembers at most 10,000 tenants (codamai.cias.tenant-gate.max-entries).

What the gate answers
Remembered answerCIAS reachableCIAS saysResult
younger than 30 s––the remembered answer, without asking
none or olderyesservedadmitted, remember
none or olderyesnot served403 tenant-not-served, remember
none or olderyesunknown403 tenant-not-served, remember
present, no matter how oldno–the remembered answer, even if it was no
noneno–403 tenant-not-served

The rule for an outage in one sentence: An outage of CIAS does not throw out anyone who was already working, and does not let anyone new in. “Keep serving” means keeping the last answer, not assuming a favorable one. A tenant that was last suspended stays suspended during the outage.

Any error while asking counts as unreachable: a timeout, a refused connection, an answer with an error code, an unreadable answer. None of them is safer than the others, so all are treated the same.

Unknown and suspended look the same

Why the gate refuses

When: CIAS knows nordbau but does not serve it.

The gate refuses.

Result: 403 cias.authentication.tenant-not-served

When: CIAS knows no tenant nordbau.

The gate refuses, with exactly the same answer.

Result: 403 cias.authentication.tenant-not-served

When: The token contains, for example, Nordbau with uppercase letters.

The gate does not even ask and treats it like an unknown tenant.

Result: 403 cias.authentication.tenant-not-served

When: The question fails, and there is no remembered answer for nordbau.

The gate refuses.

Result: 403 cias.authentication.tenant-not-served

When: The tenant is admitted, but the person's values in this tenant cannot be read.

The filter chain refuses instead of continuing with wrong or empty values. An empty value would be just one * away from switching off an attribute filter.

Result: 403 cias.authentication.tenant-not-served

All refusals carry the same key. This is on purpose: if the answer revealed whether a tenant exists, anyone with a valid token could try out company names and so query the customer list. You find the exact reason in the application log. More on this in Rejections that reveal nothing.

A suspension takes effect with a delay

sequenceDiagram
    participant A as Admin
    participant C as CIAS
    participant G as Tenant gate
    participant K as Client of nordbau
    K->>G: Request (0 s)
    G->>C: Is nordbau served?
    C-->>G: yes, remembered for 30 s
    A->>C: suspend nordbau (10 s)
    K->>G: Request (20 s)
    G-->>K: remembered: yes, continues
    K->>G: Request (35 s)
    G->>C: Is nordbau served?
    C-->>G: no
    G-->>K: 403 tenant-not-served

So a suspension takes effect at the latest after the configured time, in both operating modes. The same applies the other way round: if the gate has just remembered “unknown” and someone creates the tenant right now, it is admitted only after the time has passed.

The time is therefore a security setting, not only a question of performance. Longer means fewer lookups, but also suspended customers who keep working longer.

Pitfalls

Where to go next

Sources in the code and the knowledge base
  • CIAS/cias-authentication – TenantGate (admit, cache, outage rule, remember, invalidate), TenantGateProperties (codamai.cias.tenant-gate.ttl 30s, max-entries 10000), TenantAdmission (ADMITTED, NOT_SERVED, UNKNOWN, LOOKUP_UNAVAILABLE), TokenParser.admit, RequestAdmission.TENANT_NOT_SERVED
  • CIAS/cias-kernel – TenantLookupPort, TenantStanding, TenantKey.isValid
  • CIAS/cias-tenancy – LocalTenantLookupAdapter, TenantService.standing, Tenant.isServedOn, TenantLookupController (GET /cias/lookup/tenants/{key}), TenantLookupRoles, CiasTenancyConfiguration (lookup, lookup-rest)
  • CIAS/cias-tenancy-client – RemoteTenantLookupAdapter, CiasTenancyClientProperties (base-url, token, connect-timeout 2s, request-timeout 2s)
  • CIAS/cias-authentication/docs/adr – ADR-021; CIAS/cias-kernel/docs/adr – ADR-022
Search