Azure / Azure/data-api-builder

Design: Authorization-context-isolated result caching (OBO / session-context)

Open
#3,774 0 comments 0 reactions 2 assignees Claimed by @croblesm View on GitHub
cri
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.