Epic: #5885 | Added: 2026-07-29 per ultimate-user feedback
Depends on: #5895 (Epic 1 gate). Blocks: #5903.
What
Beyond ~200 group memberships, Entra omits the groups claim and emits overage markers (_claim_names/hasgroups/groups:srcN), which would otherwise look like "user in no groups." The SSO browser flow already solves this with a Graph fallback (sso_service.py:331-462); this story brings equivalent support to the trust-mode request path:
- Extract the overage-marker detection (sso_service.py:1444-1453) into a helper shared by the SSO enrichment path (behavior unchanged) and the trust-mode claims module.
- App-only Microsoft Graph client for trust mode: client-credentials token acquisition reusing the SSO provider record's stored (encrypted) client credentials, then
POST /users/{oid}/getMemberObjects (securityEnabledOnly: true), bounded by timeout and max-groups settings (generalizing sso_entra_graph_api_*).
oid-keyed group-resolution cache (Redis when available; namespaced per the auth-cache work in Epic 1; TTL bounded by the presenting token's exp) — the SSO path calls Graph once per login, but trust mode runs per request, so caching is mandatory.
- Policy wiring per
jwt_trust_overage_policy: graph_lookup → resolve via client (cached); fail_closed (default) → 401 with actionable error; proceed_without_groups → continue with empty groups + warning (parity with current SSO behavior).
Must NOT
- Never use the inbound bearer token for Graph calls — it is audience-bound to ContextForge and Graph will reject it.
- No unbounded lookups; no changes to default-mode or SSO browser-flow behavior.
Acceptance criteria
uv run pytest tests -k "overage or graph" -q green, including the three-policy matrix and cache assertions (second identical request served from cache — single Graph call).
- Existing SSO enrichment tests pass unmodified (shared-helper refactor is behavior-identical).
- Graph acquisition failure under
graph_lookup → 401 (fail-closed); under proceed_without_groups → documented degraded behavior.
- AC-extra-1 (cache-error semantics): When Redis raises an error during cache lookup and the policy is
graph_lookup, the implementation MUST treat the error as a cache miss and attempt a live Graph call (not an immediate 401). If the Graph call also fails, 401 is returned. Tests must assert: Redis mock raises ConnectionError + Graph mock succeeds → request authorised; Redis mock raises ConnectionError + Graph mock fails → 401.
- AC-extra-2 (proceed_without_groups warning log): When
jwt_trust_overage_policy = proceed_without_groups, a WARNING-level structured log entry MUST be emitted on every overage-triggered request, carrying the user's oid and a note that group resolution was skipped. This must be asserted in the test matrix row for this policy.
Epic: #5885 | Added: 2026-07-29 per ultimate-user feedback
Depends on: #5895 (Epic 1 gate). Blocks: #5903.
What
Beyond ~200 group memberships, Entra omits the
groupsclaim and emits overage markers (_claim_names/hasgroups/groups:srcN), which would otherwise look like "user in no groups." The SSO browser flow already solves this with a Graph fallback (sso_service.py:331-462); this story brings equivalent support to the trust-mode request path:POST /users/{oid}/getMemberObjects(securityEnabledOnly: true), bounded by timeout and max-groups settings (generalizingsso_entra_graph_api_*).oid-keyed group-resolution cache (Redis when available; namespaced per the auth-cache work in Epic 1; TTL bounded by the presenting token'sexp) — the SSO path calls Graph once per login, but trust mode runs per request, so caching is mandatory.jwt_trust_overage_policy:graph_lookup→ resolve via client (cached);fail_closed(default) → 401 with actionable error;proceed_without_groups→ continue with empty groups + warning (parity with current SSO behavior).Must NOT
Acceptance criteria
uv run pytest tests -k "overage or graph" -qgreen, including the three-policy matrix and cache assertions (second identical request served from cache — single Graph call).graph_lookup→ 401 (fail-closed); underproceed_without_groups→ documented degraded behavior.graph_lookup, the implementation MUST treat the error as a cache miss and attempt a live Graph call (not an immediate 401). If the Graph call also fails, 401 is returned. Tests must assert: Redis mock raisesConnectionError+ Graph mock succeeds → request authorised; Redis mock raisesConnectionError+ Graph mock fails → 401.jwt_trust_overage_policy = proceed_without_groups, aWARNING-level structured log entry MUST be emitted on every overage-triggered request, carrying the user'soidand a note that group resolution was skipped. This must be asserted in the test matrix row for this policy.