Azure / Azure/data-api-builder
Design: Authorization-context-isolated result caching (OBO / session-context)
- Dominant language
- C#
- Stars
- 1.5k
- Forks
- 370
- Avg merge
- 3d 17h
- Merged PRs (30d)
- 8
Description
## Problem
Result caching is currently forced off when **OBO** (`user-delegated-auth`) or MSSQL **`set-session-context`** is enabled:
- OBO: config validation requires `runtime.cache.enabled = false`
- Session-context: `CanUseCache()` returns false so the cache is never used
That is **safe but overly conservative**. The database identity is already isolated per request, yet operators must choose between per-user authorization and caching (or run dual instances).
Database policies (`@claims.*`) also cause many read paths to skip the cache even when caching is otherwise enabled.
## Proposal
Partition result-cache keys by an **authorization-context fingerprint** so cached results cannot cross security boundaries:
```text
ResultCacheKey = QueryFingerprint + auth:{Fingerprint} + v:{EntityVersion}
```
- **Fingerprint inputs:** client role (`X-MS-API-ROLE`), subject (`oid` / `sub`), tenant (`tid`), and either policy-referenced claims (when static analysis is complete) or the full non-volatile principal (session-context / incomplete analysis).
- **Fail-closed:** if the fingerprint cannot be computed, that request skips cache get and set.
- **Mutations:** bump a per-entity version token so all auth partitions miss after a write (same process).
- **Non-isolated entities:** keep the existing key shape (no `auth:` segment).
## Security invariant
Share a cache entry **only** when every result-affecting security input matches. The previous "disable cache under OBO/session-context" rule is replaced by **explicit isolation**, not by weakening isolation.
## Scope (v1)
- SQL query path (REST + GraphQL via `SqlQueryEngine`)
- OBO and `set-session-context` are **MSSQL** features; other engines keep current semantics aside from the shared entity-version segment when an entity name is supplied
- Opt-in via existing `runtime.cache.enabled` / entity cache settings (defaults unchanged)
## Proposed v1 decisions (for maintainer alignment)
| Open question | Proposed v1 answer |
| :--- | :--- |
| Multi-instance / L2 Redis invalidation | **Process-local entity version + TTL.** Document that cross-replica staleness is bounded by cache TTL. Shared Redis version key is a **follow-up** when L2 is configured. |
| `@claims.*` static analysis | **Regex extractor at config load** for `@claims.` in database policy strings. Richer policy AST analyzer only if review finds gaps. |
| Observability | **Logging only in v1** (e.g. fail-closed at Warning). Metrics for isolated hit/miss / fail-closed as a follow-up. |
### Multi-instance wording (proposed for the design contract)
> **Multi-instance invalidation:** Entity version counters are process-local in the initial design. Cross-replica staleness is bounded by cache TTL. A follow-up can move the version token into L2 (Redis) when distributed cache is enabled (`INCR` on mutation, include value in the key on read), preserving the same key shape and fail-closed behavior without requiring per-partition eviction.
## Validation status (prototype)
- Unit coverage for fingerprint isolation, fail-closed, claim analysis, and config validation
- Live Entra OBO + cache smoke on MSSQL
- Full multi-engine CI and a dedicated TestServer JWT suite remain follow-ups
Feedback on the isolation model and the multi-instance invalidation approach would be especially useful before an implementation PR.
Contributor guide
Assessment
This issue has not been assessed yet.