You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
New module (e.g. mcpgateway/utils/trusted_claims.py) implementing configurable claim extraction per #5898 settings and the PRINCIPAL CONTRACT (documented in-module):
user_id (required, opaque string, NO UUID heuristic in trust mode)
email (optional attribute — audit/observability degrade to the "unknown" sentinel when absent, NEVER a None write: ObservabilityTrace.user_email is nullable (db.py:2894) but AuditTrail.user_id is nullable=False (db.py:6631-6632), so the sentinel string is mandatory on that path)
full_name (optional, default None)
teams (pinned shape: list of strings OR list of {id,name} — normalize both, matching auth.py:1640-1643/1904-1907 branch behavior)
roles (list of role names — RESOLUTION RULE, pinned: role names resolve to permission sets via the server-side roles table ONLY (db.py:1154-1205; reading roles is allowed, it is not email_users); permissions are NEVER embedded in or read from the token; unknown role names are ignored with a warning log (fail-closed); team-scoped role names apply within the teams the token carries)
is_admin (bool, default False)
auth_provider ("jwt-trust" or issuer)
token_use (new value "trusted")
Returns a virtual principal object/dict exposing the same attributes downstream consumers read (.email, .is_admin, .full_name) — precedents: _bootstrap_platform_admin_user (auth.py:1298-1316), _user_from_cached_dict. TDD: failing tests for every mapping incl. custom claim names (Entra "groups", Keycloak "realm_access.roles" style nested paths if supported — decide and document support level).
Per requirements review, the virtual principal contract must also consume role annotations produced by the group mapping resolver (#5976):
resolve_external_groups_to_teams now returns both (team_ids, role_names). When cf_role is set on a mapping row, the associated role name is included in role_names.
This module MUST merge role_names from the resolver into the roles list before resolution against the server-side roles table. The merged list is processed identically to inline claim roles (unknown names ignored with a warning, fail-closed).
uv run pytest tests -k "trusted_claims or claims" -q green
Contract doc-comment enumerates every field + optionality
Nested-claim support level explicitly tested (either works or documented-unsupported with clear error)
Resolver-supplied role names are merged with claim roles before server-side resolution; test asserts that a principal with no roles claim but a mapping row with cf_role=developer resolves to the developer permission set.
QA scenarios
Happy: full-claim token yields complete principal
Failure: missing sub/user_id → extraction error (never defaults to email), missing email → principal with email=None and identity intact
Group→role: token with no roles claim + mapping row with cf_role → Layer-2 permission check passes for a2a.invoke
Commit: feat(auth): add configurable claims extraction and virtual principal contract
Parent epic: #5885
Depends on: #5896, #5898, #6272. Blocks: #5900, #5902, #5903. Parallel with: #5897.
What to do
New module (e.g. mcpgateway/utils/trusted_claims.py) implementing configurable claim extraction per #5898 settings and the PRINCIPAL CONTRACT (documented in-module):
rolestable ONLY (db.py:1154-1205; reading roles is allowed, it is not email_users); permissions are NEVER embedded in or read from the token; unknown role names are ignored with a warning log (fail-closed); team-scoped role names apply within the teams the token carries)Returns a virtual principal object/dict exposing the same attributes downstream consumers read (.email, .is_admin, .full_name) — precedents: _bootstrap_platform_admin_user (auth.py:1298-1316), _user_from_cached_dict. TDD: failing tests for every mapping incl. custom claim names (Entra "groups", Keycloak "realm_access.roles" style nested paths if supported — decide and document support level).
Amendment (2026-07-29)
Per ultimate-user feedback, the contract gains:
_claim_namescontaininggroups,hasgroups,groups:srcNkeys, string-typedgroupsclaim) — extracted into a helper shared with the SSO enrichment path (sso_service.py:1444-1453; SSO behavior unchanged). Resolution behavior perjwt_trust_overage_policy(Config surface - JWT-trust mode + claim-mapping settings #5898); the Graph client itself is [FEATURE]: Entra group-overage support - app-only Graph client + resolution cache #5977.jti,utisupported).token_teamsdirectly — they feed the mapping resolver ([FEATURE]: External group to team mapping seam + in-core mapping table #5976).Amendment (2026-07-31) — group→role annotations
Per requirements review, the virtual principal contract must also consume role annotations produced by the group mapping resolver (#5976):
resolve_external_groups_to_teamsnow returns both(team_ids, role_names). Whencf_roleis set on a mapping row, the associated role name is included inrole_names.role_namesfrom the resolver into theroleslist before resolution against the server-siderolestable. The merged list is processed identically to inline claim roles (unknown names ignored with a warning, fail-closed).a2a.invoke(via thedeveloperorviewerrole) through Entra group membership alone, without requiring the token to carry a nativerolesclaim. Full design is tracked in [BUG]: /admin/llm/* and /v1/admin/llm/* diverge on CSRF validation immediately after login #5978.Must NOT do
Do not touch get_current_user yet.
References
Acceptance criteria
uv run pytest tests -k "trusted_claims or claims" -qgreenrolesclaim but a mapping row withcf_role=developerresolves to thedeveloperpermission set.QA scenarios
rolesclaim + mapping row withcf_role→ Layer-2 permission check passes fora2a.invokeCommit: feat(auth): add configurable claims extraction and virtual principal contract