feat(auth): add team scope support for session tokens#3217
feat(auth): add team scope support for session tokens#3217crivetimihai merged 10 commits intomainfrom
Conversation
|
@crivetimihai I confirm that my changes are intact in this PR |
c45981a to
c88acf6
Compare
jonpspri
left a comment
There was a problem hiding this comment.
Approval Summary
Reviewed the full changeset (original commits + refactor commit). The final state is clean and correct.
What changed
resolve_session_teams()extracted inmcpgateway/auth.pyas the single policy point for session-token team resolution. Session tokens always resolve membership from DB/cache, ensuring revoked team memberships take effect immediately.- Three call sites (auth.py cached path, auth.py fallback path, token_scoping middleware) now delegate to
resolve_session_teams()instead of inlining the logic or importing the private_resolve_teams_from_db. - Debug comment removed from
email_auth.py(hardcoded UUID leftover). - Docstring lint fix in
main.py(excessReturnson a-> Nonefunction). - Docs updated in
multitenancy.mdandrbac.mdto documentresolve_session_teams()alongsidenormalize_token_teams(). - Tests thoroughly cover session token DB resolution (cached + fallback paths), API token embedded teams, legacy tokens, and null/missing/multi-team edge cases.
Security review
- Session tokens never trust embedded JWT team claims — membership is always current from DB/cache. This prevents stale authorization after team removal.
- Admin bypass (
teams: null+is_admin: true) is correctly routed through_resolve_teams_from_dbwhich returnsNone. - API and legacy tokens continue to use
normalize_token_teams()directly, which is the correct behavior for immutable programmatic tokens.
Nits (non-blocking)
test_email_auth_router.pyfixture fixes (auth_provider,get_teams,password_change_enforcement_enabled) are compatibility fixes for othermainchanges, not part of #3003. Consider splitting into a separate commit on future PRs for cleaner history.
Deep-Dive Review — @KKNithin @jonpspriRebased cleanly onto Finding 1 (Critical): PR does NOT implement issue #3003's user storyIssue #3003 asks:
What the PR actually does: Commit evolution:
Jonathan's security concern is valid — but the result is that Suggested resolution — intersection approach: Resolve from DB first (ensuring revoked memberships are enforced), then intersect with JWT-embedded teams if present. This gives both the security property and the scope-narrowing user story: async def resolve_session_teams(payload, email, user_info):
db_teams = await _resolve_teams_from_db(email, user_info)
if db_teams is None: # admin bypass
return None
jwt_teams = payload.get("teams")
if isinstance(jwt_teams, list) and jwt_teams:
# Narrow to intersection — only teams the user still has in DB
narrowed = [t for t in db_teams if t in jwt_teams]
return narrowed if narrowed else db_teams
return db_teamsThis way:
Finding 2 (Medium): "Single policy point" claim has gapsThe docstring claims
The batched queries path is the default production path for first-time requests ( Suggested fix: Update Finding 3 (Low):
|
| Finding | Severity | Action needed |
|---|---|---|
| PR doesn't implement #3003 user story | Critical | Decide: intersection approach, or descope and file follow-up |
| "Single policy point" has gaps (3/6 paths bypass) | Medium | Update main.py; address batched path |
_check_team_membership reads JWT not resolved teams |
Low | Code comment or refactor |
| email_auth_router test fixes | Info | None |
The code quality, test coverage, and security posture are solid. The core question is whether the PR should actually deliver the feature #3003 requested (scope-narrowing via intersection) or accept the current DB-only approach and file a follow-up.
Addendum — cross-verified with CodexRan an independent automated review (Codex, read-only) against
Overall assessment (concurred by both reviewers): Requires rework if this PR is meant to close #3003 as written. The security refactoring is solid, but the feature isn't delivered and the docs overstate centralization. |
1728ab6 to
5355f42
Compare
|
@crivetimihai @jonpspri I am ok with is new architecture of narrowing teams, its actually better than what was implemented and make it more secure. |
Response to deep-dive review findingsThanks @crivetimihai for the thorough review and Codex cross-validation. All findings have been addressed across the commit history. Here's the current disposition: Finding 1 (Critical): PR didn't implement #3003 user story — RESOLVEDThe intersection approach you suggested is now implemented in
This delivers the #3003 user story (a user in team1+team2 can narrow their session to team1 only) while maintaining the security property that revoked memberships take effect immediately. Semantic note on empty intersection: The original suggested code fell back to full DB teams on empty intersection. We chose fail-closed ( Finding 2 (Medium): "Single policy point" had gaps — RESOLVEDAll 6 session-token resolution paths now go through the public API:
The batched path passes Finding 3 (Low):
|
jonpspri
left a comment
There was a problem hiding this comment.
Approval
All findings from the deep-dive review have been addressed. The final state is clean, correct, and well-documented.
What was verified
- DB-first intersection policy delivers the #3003 user story while maintaining security (revoked memberships enforced immediately, fail-closed on empty intersection)
- Single policy point claim is now accurate — all 6 session-token resolution paths route through
resolve_session_teams()/resolve_session_teams_sync() _check_team_membershipbypass for session tokens eliminates the JWT-vs-DB mismatch concern; staleness bounded by auth_cache TTL- Cache isolation stores raw DB teams, not per-session narrowed intersection, preventing cross-session cache poisoning
- Tests cover intersection logic, empty intersection fail-closed, cache poisoning regression (with narrowing payload), admin bypass, and direct
_narrow_by_jwt_teamsedge cases (218 tests pass) - Documentation in
multitenancy.md,rbac.md, andsecuring.mdaccurately reflects all behavioral changes including session token scoping tables,teams: []semantics, and cache TTL staleness
fd9ec62 to
f07b2c2
Compare
45249bb to
357c39e
Compare
jonpspri
left a comment
There was a problem hiding this comment.
Review: Approve
Reviewed the full diff against main, ran all relevant test suites (auth, token scoping, streamable HTTP transport, main extended, email auth router — all pass), confirmed ruff clean on changed production files, and verified single Alembic head.
What this delivers
Implements #3003: session tokens now support team scoping via a DB-first intersection approach. resolve_session_teams() is the single async policy point — the database is always authoritative, and the JWT teams claim only narrows (never broadens) the result. Fail-closed on empty intersection (all JWT-claimed teams revoked → public-only scope).
Key design decisions (all sound)
- All 6 session-token resolution paths centralized through
resolve_session_teams() - Session tokens skip
_check_team_membershipin the middleware (DB resolution makes it redundant; staleness bounded by auth_cache TTL) - Cache stores raw DB teams, not the narrowed intersection, preventing cross-session cache poisoning
_resolve_teams_from_db_syncremoved;_normalize_jwt_payloadmade async to use the shared policy point
No blocking findings
Two low/info items noted (async-only _normalize_jwt_payload, silent degradation UX on empty intersection) — neither requires action before merge.
f8f807f to
f4318a5
Compare
5cd1825 to
0368a66
Compare
0368a66 to
bbaa0a0
Compare
Signed-off-by: Nithin Katta <Nithin.Katta@ibm.com>
Signed-off-by: Nithin Katta <Nithin.Katta@ibm.com>
…only Signed-off-by: Nithin Katta <Nithin.Katta@ibm.com>
…ion tokens Signed-off-by: Nithin Katta <Nithin.Katta@ibm.com>
Signed-off-by: Nithin Katta <Nithin.Katta@ibm.com>
…coping Implement resolve_session_teams() as the single policy point for session-token team resolution. Teams are always resolved from the database first so that revoked memberships take effect immediately. If the JWT carries a non-empty teams claim, the result is narrowed to the intersection of DB teams and JWT teams — letting callers scope a session to a subset of their memberships without risking stale grants. All six session-token resolution paths (auth.py cache, batched, and fallback; token_scoping middleware; AdminAuthMiddleware; streamablehttp transport) now route through this function. Session tokens skip _check_team_membership re-validation since resolve_session_teams() already resolved membership from the DB. Remove _resolve_teams_from_db_sync (replaced by async-only path). Regenerate code2flow.svg and remove unused test mock attributes. Closes #3003 Signed-off-by: Jonathan Springer <jps@s390x.com>
The warning box previously stated admin bypass requires `teams: null` without qualifying that this applies only to API/legacy tokens. This contradicted the session token table directly above it, which correctly shows admin bypass is DB-derived regardless of JWT teams. Split the warning into API/legacy vs session token sections to eliminate the contradiction. Closes #3003 Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
The token scoping quick reference only described API/legacy token behavior. Add the session-token table and key behaviors so the project guidance reflects the dual-model introduced by #3003. Also note that token scoping (Layer 1) is independent of RBAC (Layer 2) — session-token narrowing controls visibility, not which team roles are checked for permissions. See #3799 for the Layer 2 follow-up. Closes #3003 Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Fix remaining stale references that described admin bypass without distinguishing API/legacy tokens from session tokens: - AGENTS.md:112 — security invariant now references both normalize_token_teams() and resolve_session_teams() - multitenancy.md:1247 — enforcement summary now qualifies admin bypass per token type - rbac.md:246 — "Explicit Admin Bypass" section split by token type - rbac.md:639 — scoping strategy table corrected for session tokens Closes #3003 Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Lines 1246 and 1248 described missing/empty teams behavior without distinguishing API/legacy from session tokens. For session tokens, missing/null/empty teams returns full DB membership (not public-only). Closes #3003 Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
bbaa0a0 to
9069a0a
Compare
Review ChangesRebased onto Commits added
No code changesAll original code by @KKNithin and @jonpspri is unchanged. The changes are documentation-only. Follow-up issueFiled #3799 to track narrowing RBAC Layer 2 permission checks to match session-token team scope (pre-existing behavior, out of scope for this PR). Test resultsAll 1407 tests pass across all affected test files. |
crivetimihai
left a comment
There was a problem hiding this comment.
Review: Approve (cannot self-approve via GitHub)
Code & Security: The DB-first intersection model is well-designed. resolve_session_teams() correctly centralizes all 6 session-token resolution paths. Admin bypass is properly gated. JWT teams can only narrow (never broaden) session scope. Fail-closed on empty intersections. No privilege escalation vectors found.
Tests: Comprehensive coverage — TestResolveSessionTeams (10 tests), TestNarrowByJwtTeams (8 tests), plus integration tests in middleware and transport layers. All branches exercised.
Docs: All admin bypass statements now qualified by token type. Session-token contract documented in AGENTS.md.
Known limitation (out of scope): RBAC Layer 2 check_any_team aggregation is not narrowed by token_teams — tracked in #3799.
🔗 Related Issue
Closes #3003
📝 Summary
When
teamsare part of claims in session tokens, use it to as token_teams this helps users to use team tokens functionality which they usually get when they create API tokens scoped to a particular team.🏷️ Type of Change