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.

Never accept a 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:read or jobs:submit.
  • Program membership scopes those capabilities to specific programs, so an analyst on wing-2024 cannot read nacelle-7 results.
  • 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:

ModelIsolationCost / OpsTypical CAE fit
Shared schema, tenant columnLogical onlyLowestMany small internal teams
Schema-per-tenantStrong logicalModerateRegulated programs, moderate count
Database/account-per-tenantPhysicalHighestExternal 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.

Request Path Through Auth, Policy, and Tenant Isolation Client solver job / dashboard Identity Provider OAuth2 / OIDC issues JWT API Gateway verify signature, aud, exp extract tenant_id, scope Policy Decision Point role + program + object allow / deny CAE Data Services results / jobs / export set app.tenant_id Metadata DB row-level security tenant_id policy Object Storage per-tenant prefix scoped presigned URL Quota / Queue rate + concurrency per tenant token auth authorize allowed query artifact throttle Audit Log tenant_id · sub · action · resource_id · decision (allow/deny) Every layer enforces tenant isolation independently; the audit log records all decisions.

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.

ControlWhere enforcedVerification
JWT signature, aud, exp validatedAPI gatewayReject token with wrong audience
tenant_id taken only from verified tokenGateway → service contextSend mismatched body tenant, expect 403
Row-level security on all tenant tablesDatabaseQuery without filter returns zero rows
Per-tenant storage prefix + IAM conditionObject storagePresigned URL fails on other prefix
Per-tenant rate and concurrency limitsGateway / queueBurst from one tenant yields 429, others unaffected
Audit events for allow and denyAll servicesDeny 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.