CodamAIDocs
Topicdone

Working for a tenant without a request

How background work (timers, jobs) runs for a tenant, through the same gate, and which three places may set a tenant.

Variants
tenant is servedtenant not servednestedwithout roles

What this is about

Not all work starts with a request. A timer cleans up old entries every night, a message listener processes a message from a queue, a job computes reports for every customer. This work has no token and so does not run through the filter chain, and therefore not through the tenant gate either.

That is dangerous. The persistence itself no longer checks whether a tenant is served, because the gate takes care of that. If it gets a tenant key, it routes the work into that tenant’s database, and where automatic creation is enabled, it even creates a new database for an unknown key. A job that works for a suspended or misspelled tenant would not be stopped.

How it works

@Component
class NightlyCleanup {

  private final TenantScope scope;
  private final CleanupService cleanup;

  void run(List<String> tenantKeys) {
    for (String tenant : tenantKeys) {
      try {
        // asks the gate, fills the context, restores it afterwards
        scope.runAsTenant(tenant, "svc-nightly-cleanup", cleanup::purgeOldEntries);
      } catch (TenantNotServedException refused) {
        // tenant suspended, unknown, or CIAS unreachable: skip it
      }
    }
  }
}
What TenantScope does, step by step
  1. 1
    CIAS
    checks the input: tenant, acting identifier and work must be present, the key must be valid
  2. 2
    CIAS
    asks the tenant gate: Is nordbau served? Same memory, same rule during an outage
  3. 3
    CIAS
    remembers the previous RequestContext and sets a new one: tenant nordbau, allowed tenants only nordbau, person svc-nightly-cleanup, no roles
  4. 4
    CIAS
    runs the work
  5. 5
    CIAS
    restores the previous RequestContext, even if the work ends with an error
    Result: The work ran in nordbau, and only there.

The variants

What can happen with TenantScope

When: nordbau is ACTIVE and within the validity window.

The work runs in the tenant nordbau. The persistence finds nordbau in the list of allowed tenants and routes into its database.

Result: callAsTenant returns the result of the work, runAsTenant returns nothing.

When: nordbau is suspended, closed, expired or unknown, or CIAS is unreachable and nothing is remembered.

The work does not run. TenantScope throws a TenantNotServedException, and the log says for which tenant and as whom it was refused.

Result: The caller catches the exception and continues with the next tenant.

When: A job goes over several tenants and opens its own scope for each, even inside another one.

Every scope remembers the context it found and restores exactly that one.

Result: After the inner scope, the outer one works in its tenant again.

When: The work calls code that requires a role.

The context has no roles, so every role check refuses. A job works with the authority of its own code, not with that of a person.

Result: If a job could give itself roles, any code that can name a tenant could grant itself any right.

Why an acting identifier is required

Besides the tenant, runAsTenant requires an identifier saying as whom the work runs, for example svc-nightly-cleanup. Without it TenantScope refuses. A job that changes data leaves traces in the audit, and “the system” says nothing there. The identifier appears in the revision the same way as, for a request, the person who sent it. As address and browser it records internal.

The three places that may set a tenant

Writing a tenant into the RequestContext means: deciding in which database the work happens. Exactly three places may do this, all in CIAS:

PlaceWhenWhat was checked before
Filter chain (TokenParser)on every request with a tokentoken valid, tenant resolved, gate admitted
Tenant switch (ContextSwitch)when the header tenant takes effectrealm role, target in the list of allowed tenants; afterwards the filter chain asks the gate for the target
TenantScopefor work without a requestgate admitted

Any other code that sets the tenant itself bypasses the gate. To prevent this there is an architecture rule in cias-test-support: TenantContextRules.tenantContextComesFromTheGate(). A module adds it to its tests, and the build fails as soon as a class sets the tenant in the RequestContext itself. CDMS runs this rule in its integration tests.

Pitfalls

Where to go next

Sources in the code and the knowledge base
  • CIAS/cias-authentication – TenantScope (runAsTenant, callAsTenant, contextFor), TenantNotServedException, CurrentTenantProviderImpl, CiasTokenConfiguration (bean tenantScope)
  • CIAS/cias-test-support – TenantContextRules (tenantContextComesFromTheGate, three allowed places)
  • CDMS/cdms-integrationtest – TenantContextArchitectureTest
  • commons-persistence – DatabaseRequestContext.requireAllowedTenant (empty list refuses)
  • CIAS/cias-authentication/docs/adr – ADR-021 (section 5)
Search