Skip to content

feat(session-manager): tenant validation on session access#13

Merged
andylim-duo merged 4 commits intomainfrom
feature/multi-tenant-session-manager
Mar 18, 2026
Merged

feat(session-manager): tenant validation on session access#13
andylim-duo merged 4 commits intomainfrom
feature/multi-tenant-session-manager

Conversation

@andylim-duo
Copy link
Owner

Summary

Adds tenant-scoped session isolation to StreamableHTTPSessionManager, preventing cross-tenant session hijacking. This is iteration 5 of 6 in the multi-tenancy implementation plan.

Context: Multi-Tenancy Implementation Plan

  1. Foundation — Add tenant_id to authentication tokens (done, PR feat(auth): add tenant_id field to authentication token models #2)
  2. Core Plumbing — Add tenant_id to session and request context (done, PR feat(auth): add tenant_id to session and request context #3)
  3. Managers — Implement tenant-scoped storage (done, PR feat(managers): tenant-scoped storage for ToolManager, ResourceManager, PromptManager #5 + refactor(managers): O(1) tenant lookups and missing APIs #10)
  4. MCPServer Integration — Thread tenant_id from handlers to managers (done, PR feat(server): thread tenant_id through MCPServer handlers to managers #9)
  5. Session Manager — Tenant validation on session access (this PR)
  6. Configuration, Documentation, and Final Testing

Changes

  • src/mcp/server/streamable_http_manager.py:

    • Added _session_tenants dict to track which tenant owns each session
    • On new session creation, records the tenant from tenant_id_var
    • On existing session lookup, validates the requesting tenant matches the session's bound tenant — returns 404 on mismatch (same response as "session not found" to avoid information leakage)
    • Sessions created without a tenant (None) allow access from any request (backward compatibility)
    • Tenant mappings are cleaned up alongside sessions on all exit paths: idle timeout, crash, and shutdown
  • tests/server/test_streamable_http_manager.py — 7 new tests:

    • Tenant mismatch returns 404
    • Bidirectional isolation: two tenants cannot access each other's sessions
    • Same tenant can reuse its own session
    • No-tenant sessions allow any access (backward compat)
    • Unauthenticated requests cannot access tenant-bound sessions
    • Tenant mapping cleaned up on session exit

Design Decision

The plan originally proposed composite keys (tenant_id, session_id) for session storage. This was unnecessary since session IDs are globally unique UUIDs. Instead, a simpler parallel _session_tenants dict validates tenant ownership on lookup — same security guarantee, less invasive change.

Backward Compatibility

Fully backward compatible. Sessions created without authentication (tenant_id=None) behave exactly as before — no tenant validation is applied.

Test plan

  • 7 new tenant isolation tests pass
  • All 11 existing session manager tests pass
  • 100% branch coverage on streamable_http_manager.py
  • pyright passes with 0 errors
  • ruff passes

Prevent cross-tenant session hijacking by validating that the
authenticated tenant matches the session's bound tenant on every
request. Sessions created without a tenant (no auth) remain accessible
to all requests for backward compatibility.

Adds a parallel _session_tenants dict that records the tenant_id from
tenant_id_var at session creation time. On existing session lookup, a
mismatch returns 404 (same as "session not found" to avoid information
leakage). Tenant mappings are cleaned up alongside sessions on all
exit paths: idle timeout, crash, and shutdown.

Includes 7 tests covering bidirectional isolation, same-tenant reuse,
backward compatibility, unauthenticated access rejection, and cleanup.
100% branch coverage on streamable_http_manager.py.
@andylim-duo andylim-duo self-assigned this Mar 18, 2026
andylim-duo

This comment was marked as resolved.

Add tests for _extract_session_id and _extract_status that exercise
the "skip non-start messages" and "not found" paths, replacing pragma
suppressions with actual coverage.
- Split tenant mismatch log: WARNING with session ID only, DEBUG for
  tenant values to avoid leaking sensitive data at default log levels
- _set_tenant/_reset_tenant helpers always set/reset the contextvar
  unconditionally, avoiding subtle bugs when tenant is None
- Wrap all blocking-session tests in try/finally to ensure stop.set()
  runs even on assertion failures, preventing test hangs
@andylim-duo andylim-duo merged commit a1ce448 into main Mar 18, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant