4. Authentication, authorization, and multi-tenancy for production CAE data services.
CAE analytics APIs rarely serve a single user. A production deployment typically hosts simulation results for multiple business units, external suppliers, or customer programs inside one logical platform. The moment a second tenant appears, three concerns become inseparable: proving who is calling (authentication), deciding what that caller may touch (authorization), and guaranteeing that one tenant's data, compute, and quota never leak into another's (multi-tenancy). This chapter treats them as one design problem, because in practice they fail together.
Authentication: identity at the API edge
For machine-to-machine CAE workloads, the dominant pattern is OAuth 2.0 with the client credentials grant, issuing short-lived JWT access tokens. A solver post-processing job, a dashboard, or a CI pipeline each registers as a client and exchanges its credentials for a token. Interactive engineers use the authorization code flow with PKCE through the platform's identity provider (IdP).
A CAE-specific token should carry claims that downstream services can trust without a database round trip. A representative decoded payload looks like this:
{
"iss": "https://idp.example-cae.com/",
"sub": "svc-postproc-01",
"aud": "cae-analytics-api",
"exp": 1735689600,
"scope": "results:read jobs:submit",
"tenant_id": "acme-aero",
"roles": ["analyst"],
"programs": ["wing-2024", "nacelle-7"]
}
Two rules matter. First, aud must be validated against the API's own identifier; a token minted for the reporting service must not be accepted by the results service. Second, exp should be short — 15 to 60 minutes — with refresh handled by the client, not by long-lived tokens. Validate the signature against the IdP's JWKS endpoint, cache those keys, and reject alg: none outright.
tenant_id supplied in a query string or request body as the authoritative tenant. It must come from the verified token or a server-side mapping. A client-supplied tenant identifier is the single most common cross-tenant leak vector in CAE platforms.Authorization: from roles to resource-level decisions
Authentication answers "who"; authorization answers "may this subject perform this action on this object." CAE data has a natural hierarchy — tenant → program → simulation → run → result artifact — and permissions usually inherit downward with possible overrides. A practical model combines coarse roles with fine-grained policy:
- Roles (analyst, reviewer, admin) grant capability classes such as
results:readorjobs:submit. - Program membership scopes those capabilities to specific programs, so an analyst on
wing-2024cannot readnacelle-7results. - Object-level rules handle exceptions, e.g. a supplier may read only the artifacts explicitly shared with them.
Enforce authorization at the service boundary, not in the UI. A typical check for a results download combines all three layers:
def authorize(token, artifact_id, action="read"):
claims = verify_jwt(token) # signature, aud, exp
tenant = claims["tenant_id"]
art = artifact_store.get(artifact_id) # includes tenant_id, program
if art.tenant_id != tenant:
raise Forbidden("cross-tenant access")
if action not in scope_to_actions(claims["scope"]):
raise Forbidden("missing scope")
if art.program not in claims.get("programs", []):
raise Forbidden("program not granted")
return True
The critical line is the tenant comparison. Every data-access path — REST handlers, GraphQL resolvers, background workers, export jobs — must pass through an equivalent check. Centralizing it in a policy decision point (an internal authorize() service or an OPA sidecar) prevents the drift that appears when each endpoint reimplements its own logic.
Multi-tenancy: isolation is a spectrum
Multi-tenancy is not binary. Three deployment models trade cost against isolation strength:
| Model | Isolation | Cost / Ops | Typical CAE fit |
|---|---|---|---|
| Shared schema, tenant column | Logical only | Lowest | Many small internal teams |
| Schema-per-tenant | Strong logical | Moderate | Regulated programs, moderate count |
| Database/account-per-tenant | Physical | Highest | External customers, IP-sensitive OEM data |
For CAE specifically, result artifacts are large (often gigabytes per run) and often contractually restricted. That pushes many platforms toward a hybrid: shared metadata database with a mandatory tenant_id column, but per-tenant object-storage prefixes and per-tenant encryption keys. The storage layer then enforces isolation even if an application bug forgets a filter.
Two enforcement techniques are worth adopting regardless of model. First, row-level security in the database, so that even a query missing a WHERE tenant_id = ? clause returns nothing:
ALTER TABLE simulation_runs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON simulation_runs
USING (tenant_id = current_setting('app.tenant_id')::text);
The application sets app.tenant_id from the verified token at the start of each request or job. Second, per-tenant object-storage prefixes with IAM conditions, so a presigned URL is scoped to s3://cae-results/acme-aero/... and cannot be replayed against another prefix.
Quotas, noisy neighbors, and compute isolation
CAE post-processing is bursty: a single engineer may fan out hundreds of parallel result reads. Without per-tenant quotas, one tenant starves the rest. Apply limits at three levels — request rate (e.g. 50 req/s per tenant), concurrent jobs (e.g. 20 solver submissions), and storage bytes. Return 429 Too Many Requests with a Retry-After header rather than silently degrading. For heavy compute, route jobs to per-tenant queues or node pools so a runaway batch cannot consume shared worker capacity.
Auditing and failure symptoms
Every authorization decision — allow and deny — should emit a structured audit event containing tenant_id, sub, action, resource_id, and decision. This is what lets you answer "who read this OEM's wing data last Tuesday" during an incident review.
Common failure symptoms and their usual causes:
- Intermittent 403 on valid tokens — clock skew between IdP and API, or JWKS cache not refreshed after key rotation.
- Data from another tenant visible in a list endpoint — a query path bypassing the policy layer, often a new export or search endpoint.
- Presigned URLs working across tenants — prefix scoping missing from the IAM policy.
- One tenant's jobs delaying everyone — no per-tenant concurrency cap or shared queue.
The following diagram summarizes how a request flows through the identity, policy, and isolation layers, and where each control is enforced.
Putting it together: a minimal production checklist
Before exposing a CAE analytics API to more than one tenant, confirm each item below. These are the controls that, when absent, produce the incidents described earlier.
| Control | Where enforced | Verification |
|---|---|---|
JWT signature, aud, exp validated | API gateway | Reject token with wrong audience |
tenant_id taken only from verified token | Gateway → service context | Send mismatched body tenant, expect 403 |
| Row-level security on all tenant tables | Database | Query without filter returns zero rows |
| Per-tenant storage prefix + IAM condition | Object storage | Presigned URL fails on other prefix |
| Per-tenant rate and concurrency limits | Gateway / queue | Burst from one tenant yields 429, others unaffected |
| Audit events for allow and deny | All services | Deny event appears with correct tenant |
Treat these as test cases, not documentation. A cross-tenant access test that runs in CI — attempting to read another tenant's artifact with a valid token — will catch the regression that a code review misses. In production CAE platforms, isolation is maintained by continuous verification, not by a one-time configuration.