Multi-Tenancy ArchitectureΒΆ
ContextForge implements a comprehensive multi-tenant architecture that provides secure isolation, flexible resource sharing, and granular access control. This document describes the complete multi-tenancy design, user lifecycle, team management, and resource scoping mechanisms.
OverviewΒΆ
The multi-tenancy system is built around teams as the primary organizational unit, with users belonging to one or more teams, and all resources scoped to teams with configurable visibility levels.
Core PrinciplesΒΆ
- Team-Centric: Teams are the fundamental organizational unit for resource ownership and access control
- User Flexibility: Users can belong to multiple teams with different roles in each team
- Resource Isolation: Resources are scoped to teams with explicit sharing controls
- Invitation-Based: Team membership is controlled through invitation workflows
- Role-Based Access: Users have roles (Owner, Member) within teams that determine their capabilities
- Platform Administration: Separate platform-level administration for system management
Global Root ConfigurationΒΆ
MCP roots are global, process-local platform configuration. They are not team-owned resources in this release: team-scoped identities cannot read or mutate roots, even when a role grants admin.system_config. Team-owned roots and team_id filtering are intentionally deferred.
Root registration is disabled by default because ROOT_ALLOWED_SCHEMES defaults to an empty list. Operators must explicitly allow supported schemes. file:// roots additionally require ROOT_ALLOW_FILE_SCHEME=true and configured allowed file prefixes. Invalid DEFAULT_ROOTS fail startup; deployments using defaults must configure matching root policy before upgrade or restart.
User Lifecycle & AuthenticationΒΆ
User Authentication FlowΒΆ
sequenceDiagram
participant U as User
participant G as Gateway
participant SSO as SSO Provider
participant DB as Database
participant E as Email Service
alt Email Authentication
U->>G: POST /auth/email/login
G->>DB: Validate email/password
DB-->>G: User record
G-->>U: JWT token + session
else SSO Authentication
U->>G: GET /auth/sso/login/github
G->>SSO: OAuth redirect
U->>SSO: Authorize application
SSO->>G: OAuth callback with code
G->>SSO: Exchange code for token
SSO-->>G: User profile data
G->>DB: Create/update user
G->>DB: Create personal team
G-->>U: JWT token + session
end
Note over G,DB: Personal team auto-created for new users User Creation & Personal TeamsΒΆ
When personal teams are enabled (default), each user gets an automatically created Personal Team upon registration:
flowchart TD
A[New User Registration] --> B{Authentication Method}
B -->|Email| C[Email Registration]
B -->|SSO| D[SSO Registration]
C --> E[Create EmailUser Record]
D --> F[Create SSO User Record]
E --> G[Create Personal Team<br/>if enabled]
F --> G
G --> H[Set User as Team Owner]
H --> I[User Can Access System]
subgraph "Personal Team Properties"
J[Name: Display Name's Team<br/>full name or email prefix]
K[is_personal: true]
L[Owner: User]
M[Members: User only]
N[Visibility: private]
end
G --> J
G --> K
G --> L
G --> M
G --> N
style G fill:#e1f5fe
style J fill:#f3e5f5
style K fill:#f3e5f5
style L fill:#f3e5f5
style M fill:#f3e5f5
style N fill:#f3e5f5 Team Architecture & ManagementΒΆ
Team Structure & RolesΒΆ
erDiagram
EmailTeam ||--o{ EmailTeamMember : has
EmailUser ||--o{ EmailTeamMember : belongs_to
EmailTeam ||--o{ EmailTeamInvitation : has_pending
EmailUser ||--o{ EmailTeamInvitation : invited_by
EmailTeam ||--o{ EmailTeamJoinRequest : join_requests
EmailUser ||--o{ EmailTeamJoinRequest : requested_by
EmailTeam {
string id PK
string name
string slug
string description
string created_by FK
boolean is_personal
string visibility
int max_members
boolean is_active
timestamp created_at
timestamp updated_at
}
EmailUser {
string email PK
string full_name
boolean is_admin
boolean is_active
string auth_provider
timestamp created_at
timestamp updated_at
}
EmailTeamMember {
string id PK
string team_id FK
string user_email FK
string role
string invited_by FK
timestamp joined_at
boolean is_active
}
EmailTeamInvitation {
string id PK
string team_id FK
string email
string role
string invited_by FK
timestamp invited_at
timestamp expires_at
string token
boolean is_active
}
EmailTeamJoinRequest {
string id PK
string team_id FK
string user_email FK
string status
timestamp requested_at
timestamp expires_at
timestamp reviewed_at
string reviewed_by FK
} Team Visibility & Access ModelΒΆ
flowchart TB
subgraph "Team Visibility Types"
T1["Private Team
Not discoverable; invite-only"]
T2["Public Team
Discoverable; membership by invite/request"]
end
subgraph "Team Roles"
R1["Owner
- Full team control
- Invite/remove members
- Manage resources
- Delete team"]
R2["Member
- Access team resources
- Create resources
- No member management"]
end
subgraph "Team Membership Flow"
A[User Exists] --> B{Team Type}
B -->|Private| C[Requires Invitation]
B -->|Public| D[Discover and Request Join]
C --> E[Owner Sends Invite]
E --> F[Pending Invitation]
F --> G[User Accepts/Declines]
D --> J[Join Request Pending]
J --> K[Owner Approves]
K --> H[User Joins Team]
G -->|Accept| H
H --> I[Team Member]
end
style T1 fill:#ffebee
style T2 fill:#e8f5e8
style R1 fill:#fff3e0
style R2 fill:#f3e5f5 Team Membership Levels (Design)ΒΆ
Note: These are team membership levels, separate from RBAC roles. A user can have both a membership level and RBAC role assignments within the same team.
-
Owner (Team Membership Level):
-
Manage team settings (name, description, visibility) and lifecycle (cannot delete personal teams).
- Manage membership (invite, accept, change roles, remove members).
-
Full control over team resources (create/update/delete), subject to platform policies.
-
Member (Team Membership Level):
-
Access and use team resources; can create resources by default unless policies restrict it.
- Cannot manage team membership or teamβlevel settings.
Platform Admin is a global RBAC role (not a team membership level) with systemβwide oversight.
Team Invitation WorkflowΒΆ
sequenceDiagram
participant O as Team Owner
participant G as Gateway
participant DB as Database
participant I as Invited User
Note over O,I: Invitation Process
O->>G: POST /teams/{team_id}/invitations
Note right of O: {email, role, expires_in}
G->>DB: Check team ownership
DB-->>G: Owner confirmed
G->>DB: Create invitation record
DB-->>G: Invitation token generated
G-->>O: Invitation created (token)
O-->>I: Share invite link (out of band)
Note over I,G: Acceptance Process
I->>G: POST /teams/invitations/{token}/accept
G->>DB: Validate token + user
G->>DB: Create team membership
G->>DB: Deactivate invitation (is_active=false)
G-->>I: Welcome to team Visibility SemanticsΒΆ
This section clarifies what Private and Public mean for teams, and what Private/Team/Public mean for resources across the system.
Team Visibility (Design)ΒΆ
-
Private:
-
Discoverability: Not listed to nonβmembers; only visible to members/owner.
- Membership: By invitation from a team owner (requestβtoβjoin is not exposed to nonβmembers).
-
API/UI: Team shows up only in the current user's teams list; direct deep links require membership.
-
Public:
-
Discoverability: Listed in public team discovery views for all authenticated users.
- Membership: Still requires an invitation or explicit approval of a join request.
- API/UI: Limited metadata may be visible without membership; all management and resource operations still require membership.
Note: Platform Admin is a global role and is not a team role. Admins can view/manage teams for operational purposes irrespective of team visibility.
Resource Visibility (Design)ΒΆ
Applies to Tools, Servers, Resources, Prompts, and A2A Agents. All resources are owned by a team (team_id) and created by a user (owner_email).
-
Private:
-
Who sees it: Only the resource owner (owner_email), and only when their token is not a public-only token (token_teams must be non-empty).
- Team members cannot see or use it unless they are the owner.
- Public-only tokens (token_teams=[]) cannot access private resources even as the owner.
- Platform Admin bypass (token teams=null, is_admin=true) does NOT grant visibility or mutation access to another user's private resources (see #4341). Since direct-ID reads return 404 under bypass, update/delete of another user's private resource also fails because the read-before-write gate hides the resource.
-
Mutations: Only the owner. If cross-user administrative access is required, prefer changing visibility to
teamor using a properly scoped token; admin bypass alone is insufficient. -
Team:
-
Who sees it: All members of the owning team (owners and members).
-
Mutations: Owner can update/delete; team owners can administratively manage; Platform Admin can override.
-
Public:
-
Who sees it: All authenticated users across the platform (crossβteam visibility).
- Mutations: Only the resource owner, team owners, or Platform Admins can modify/delete.
Enforcement summary:
- Listing queries include resources where (a) visibility == public, (b) team_id β token_teams with visibility β {team, public}, or Β© owner_email == user.email with visibility == private.
- Owner-based access applies only to private resources. Owners cannot use ownership to bypass team scoping for team-visibility resources outside their token scope.
- Public-only tokens (token_teams=[]) see only public resources β owner and team access are both suppressed.
- Read follows the same rules as list; write operations require ownership or delegated/team administrative rights.
Resource URI uniquenessΒΆ
Applies to the Resources entity specifically, not to the other entity types listed above. Per the MCP specification, uri is the resource identifier and name is a human-readable display label β so URIs are constrained and names are deliberately not unique (a name-uniqueness constraint was added in #5158 and reverted in #5664 because it broke legitimate task-scoped resources sharing a type name).
Uniqueness is enforced at two levels, and they are not identical:
Database (mcpgateway/db.py, Resource.__table_args__) β visibility is not part of any key:
| Constraint | Key | Applies when |
|---|---|---|
uq_team_owner_gateway_uri_resource | (team_id, owner_email, gateway_id, uri) | Always |
uq_team_owner_uri_resource_local (partial unique index) | (team_id, owner_email, uri) | gateway_id IS NULL |
Application pre-check (mcpgateway/services/resource_service.py) β runs before the insert so a collision surfaces as a 409 with a specific message rather than a raw IntegrityError. This check is visibility-dependent and is narrower or wider than the DB key depending on visibility:
| Visibility | Pre-check key on create |
|---|---|
public | (uri, visibility, gateway_id) β collides across teams and owners |
team | (uri, visibility, team_id, gateway_id) |
private | (uri, visibility, owner_email, team_id, gateway_id) |
On update the public and team branches match on (uri, visibility[, team_id]) without gateway_id; only the private branch includes it. Anything the pre-check misses is still caught by the database constraints above and reported as 409 via ErrorFormatter.format_database_error.
Token Scoping ModelΒΆ
How Token Scoping WorksΒΆ
Token scoping is the mechanism that determines which resources a user can see based on the teams claim in their JWT token.
Key functions in mcpgateway/auth.py:
normalize_token_teams()β The single source of truth for interpreting JWT team claims into a canonical form. Used directly by API tokens and legacy tokens.resolve_session_teams()β 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-emptyteamsclaim, 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. If the intersection is empty (e.g. all JWT-claimed teams have been revoked), an empty list is returned, which demotes the session to public-only scope β the user can still access public resources and their own private resources, but loses access to all team-scoped resources. An explicitteams: [](empty list) in the JWT is treated as "no restriction requested" and returns the full DB membership β this intentionally differs fromnormalize_token_teamswhere[] β public-only. IfemailisNoneor empty, returns[](public-only) β an identity-less session token never receives admin bypass._narrow_by_jwt_teams()β Shared intersection logic used byresolve_session_teams.- API tokens and legacy tokens always use
normalize_token_teams()directly.
Secure-First DefaultsΒΆ
The token scoping system follows a secure-first design principle: when in doubt, access is restricted.
API Tokens and Legacy TokensΒΆ
These use normalize_token_teams() directly β the JWT teams claim is the sole authority:
JWT teams State | is_admin | Result | Access Level |
|---|---|---|---|
| Key MISSING | any | [] | PUBLIC-ONLY (secure default) |
Key null | true | None | ADMIN BYPASS (unrestricted) |
Key null | false | [] | PUBLIC-ONLY (no bypass) |
Key [] (empty) | any | [] | PUBLIC-ONLY |
Key ["t1", "t2"] | any | ["t1", "t2"] | TEAM-SCOPED |
Session TokensΒΆ
Session tokens use resolve_session_teams() β the database is the authority, and the JWT teams claim only narrows (never broadens) the result:
JWT teams State | DB Teams | Result | Access Level |
|---|---|---|---|
| Key MISSING | ["t1", "t2"] | ["t1", "t2"] | Full DB membership |
Key null | ["t1", "t2"] | ["t1", "t2"] | Full DB membership |
Key [] (empty) | ["t1", "t2"] | ["t1", "t2"] | Full DB membership (no restriction requested) |
Key ["t1"] | ["t1", "t2"] | ["t1"] | Narrowed to intersection |
Key ["revoked"] | ["t1", "t2"] | [] | Empty intersection β public-only (fail-closed) |
| any | None (admin) | None | ADMIN BYPASS (DB authority) |
Critical Security Behavior
A missing teams key always results in public-only access for API/legacy tokens, even for admin users. Admin bypass requires explicit teams: null combined with is_admin: true.
Session Token Membership Staleness
Session tokens skip the _check_team_membership re-validation in the token scoping middleware because resolve_session_teams() already resolved membership from the database. Membership staleness is bounded by the auth_cache TTL. The cache stores the full DB membership (not the per-session narrowed intersection) so that multiple sessions for the same user narrow independently.
External IdP tokens resolve via the session table
Access tokens accepted from trusted external SSO providers (SSO_API_TOKEN_AUTH_ENABLED) are JIT-provisioned into a token_use="session" identity and follow the Session Tokens row above: is_admin and teams come from the persisted local user record via resolve_session_teams(), not from claims in the external token. See SSO: Machine-to-machine API auth with external IdP tokens.
Multi-Team Token BehaviorΒΆ
When a token contains multiple teams (teams: ["team-1", "team-2"]):
- Listing queries: Return resources visible to ANY of the user's teams
request.state.team_id: Set to the first team in the list- Resource creation: Uses
request.state.team_idas default if not explicitly specified
This means the first team in the token's teams array has special significance for default resource ownership.
Return Value SemanticsΒΆ
The normalize_token_teams() and resolve_session_teams() functions return:
| Return Value | Meaning | Query Behavior |
|---|---|---|
None | Admin bypass | Skip ALL team filtering |
[] (empty list) | Public-only | Filter to visibility='public' ONLY |
["t1", "t2"] | Team-scoped | Filter to team resources + public |
Two-Layer Security ModelΒΆ
ContextForge implements two distinct security layers that work together:
Layer 1: Token Scoping (Data Filtering)ΒΆ
Purpose: Controls what resources a user CAN SEE
- Enforced by
TokenScopingMiddlewareand service layer queries - Based on the
teamsclaim in the JWT token - Filters database queries based on visibility rules
- Applied before RBAC checks
Layer 2: RBAC (Action Authorization)ΒΆ
Purpose: Controls what actions a user CAN DO
- Enforced by
@require_permissiondecorators - Based on role assignments and permission sets
- Checks if user has permission for the requested action
- Applied after token scoping passes
Request FlowΒΆ
Request: POST /tools/{id}/execute
β
βΌ
βββββββββββββββββββββββββββββββ
β Token Scoping (Layer 1) β β Can user ACCESS this tool?
β - Check visibility β (public/team/private)
β - Check team membership β
βββββββββββββββββββββββββββββββ
β (allowed)
βΌ
βββββββββββββββββββββββββββββββ
β RBAC Permission (Layer 2) β β Can user EXECUTE tools?
β @require_permission( β (tools.execute permission)
β "tools.execute") β
βββββββββββββββββββββββββββββββ
β (allowed)
βΌ
βββββββββββββββββββββββββββββββ
β Execute Operation β
βββββββββββββββββββββββββββββββ
Enforcement Points MatrixΒΆ
| Location | Token Scoping | RBAC | Notes |
|---|---|---|---|
| Token Scoping Middleware | β | N/A | Request-level data filtering |
| REST API Endpoints | β | β | @require_permission decorators |
RPC Handler (/rpc) | β | Varies | Method-specific checks |
| Admin UI | β | β | Permission-based rendering |
| Service Layer | β | N/A | Database query filtering |
| WebSocket | β | β | Forwards auth to /rpc |
| MCP Transport | β | N/A | Streamable HTTP filtering + per-server OAuth enforcement |
Resource Scoping & VisibilityΒΆ
Resource ArchitectureΒΆ
All resources in ContextForge are scoped to teams with three visibility levels:
flowchart TD
subgraph "Resource Types"
A[MCP Servers]
B[Virtual Servers]
C[Tools]
D[Resources]
E[Prompts]
F[A2A Agents]
end
subgraph "Team Scoping"
G[team_id: UUID]
H[owner_email: string]
I[visibility: enum]
end
subgraph "Visibility Levels"
J["Private
Owner only"]
K["Team
Team members"]
L["Public
All users"]
end
A --> G
B --> G
C --> G
D --> G
E --> G
F --> G
G --> I
H --> I
I --> J
I --> K
I --> L
style J fill:#ffebee
style K fill:#e3f2fd
style L fill:#e8f5e8 Resource Visibility MatrixΒΆ
flowchart LR
subgraph "User Access to Resources"
U1["User A
Team 1 Member
Team 2 Owner"]
U2["User B
Team 1 Owner
Team 3 Member"]
U3["User C
No team membership"]
end
subgraph "Resource Visibility"
R1["Resource 1
Team 1, Private
Owner: User B"]
R2["Resource 2
Team 1, Team
Owner: User A"]
R3["Resource 3
Team 2, Public
Owner: User A"]
R4["Resource 4
Team 3, Team
Owner: User B"]
end
U1 -.->|β No Access| R1
U1 -->|β
Team Member| R2
U1 -->|β
Owner & Public| R3
U1 -.->|β Not Team Member| R4
U2 -->|β
Owner & Private| R1
U2 -->|β
Team Member| R2
U2 -->|β
Public| R3
U2 -->|β
Team Member| R4
U3 -.->|β No Access| R1
U3 -.->|β No Access| R2
U3 -->|β
Public| R3
U3 -.->|β No Access| R4
style U1 fill:#e1f5fe
style U2 fill:#f3e5f5
style U3 fill:#fff3e0 Resource Access Control LogicΒΆ
flowchart TD
A[User requests resource access] --> B{Resource visibility}
B -->|Private| C{User owns resource?}
B -->|Team| D{User in resource team?}
B -->|Public| E[β
Allow access]
C -->|Yes| F[β
Allow access]
C -->|No| G[β Deny access]
D -->|Yes| H[β
Allow access]
D -->|No| I[β Deny access]
style F fill:#e8f5e8
style H fill:#e8f5e8
style E fill:#e8f5e8
style G fill:#ffebee
style I fill:#ffebee Platform AdministrationΒΆ
Role-Based Access Control (RBAC)ΒΆ
ContextForge implements a comprehensive RBAC system with four built-in roles that are automatically created during system bootstrap. These roles provide granular permission management across different scopes.
System RolesΒΆ
The following roles are created automatically when the system starts:
1. Platform Admin (Global Scope)ΒΆ
- Permissions:
*(all permissions) - Scope: Global
- Description: Platform administrator with all system-wide permissions
- Use Case: System administrators who manage the entire platform
2. Team Admin (Team Scope)ΒΆ
-
Permissions:
-
teams.read- View team information teams.update- Modify team settingsteams.join- Join public teamsteams.manage_members- Add/remove team memberstools.read- View toolstools.execute- Execute toolsresources.read- View resources-
prompts.read- View prompts -
Scope: Team
- Description: Team administrator with team management permissions
- Use Case: Team leaders who manage team membership and resources
3. Developer (Team Scope)ΒΆ
-
Permissions:
-
teams.join- Join public teams tools.read- View toolstools.execute- Execute toolsresources.read- View resources-
prompts.read- View prompts -
Scope: Team
- Description: Developer with tool and resource access
- Use Case: Team members who need to use tools and access resources
4. Viewer (Team Scope)ΒΆ
-
Permissions:
-
teams.join- Join public teams tools.read- View toolsresources.read- View resources-
prompts.read- View prompts -
Scope: Team
- Description: Read-only access to resources
- Use Case: Team members who only need to view resources without executing them
Permission CategoriesΒΆ
The RBAC system defines permissions across multiple resource categories:
User ManagementΒΆ
users.create,users.read,users.update,users.delete,users.invite
Team ManagementΒΆ
teams.create,teams.read,teams.update,teams.delete,teams.manage_members
Tool ManagementΒΆ
tools.create,tools.read,tools.update,tools.delete,tools.execute
Resource ManagementΒΆ
resources.create,resources.read,resources.update,resources.delete,resources.share
Prompt ManagementΒΆ
prompts.create,prompts.read,prompts.update,prompts.delete,prompts.execute
Server ManagementΒΆ
servers.create,servers.read,servers.update,servers.delete,servers.manage
Token ManagementΒΆ
tokens.create,tokens.read,tokens.update,tokens.revoke
Admin FunctionsΒΆ
admin.system_config,admin.user_management,admin.security_audit
Role Assignment and ScopeΒΆ
Roles are assigned to users within specific scopes:
- Global Scope: Platform-wide permissions (platform_admin only)
- Team Scope: Team-specific permissions (team_admin, developer, viewer)
- Personal Scope: Individual user permissions (future use)
Administrator HierarchyΒΆ
flowchart TD
subgraph "RBAC Roles"
A["Platform Admin
- All permissions (*)
- Global scope
- System management"]
B["Team Admin
- Team management
- Member control
- Resource access"]
C["Developer
- Tool execution
- Resource access
- No team management"]
D["Viewer
- Read-only access
- No execution
- No management"]
end
subgraph "Domain Restrictions"
E["Admin Domain Whitelist
SSO_AUTO_ADMIN_DOMAINS"]
F["Trusted Domains
SSO_TRUSTED_DOMAINS"]
G["Admin Approval Gate
SSO_REQUIRE_ADMIN_APPROVAL"]
end
A --> E
A --> G
B --> F
subgraph "Access Hierarchy"
H[Platform Admin] --> I[All Teams & Resources]
J[Team Admin] --> K[Team Resources & Members]
L[Developer] --> M[Team Resources Only]
N[Viewer] --> O[Read-Only Access]
end
style A fill:#ff8a80
style B fill:#ffb74d
style C fill:#81c784
style D fill:#90caf9 Administrator Assignment FlowΒΆ
sequenceDiagram
participant U as New User
participant G as Gateway
participant SSO as SSO Provider
participant DB as Database
participant A as Platform Admin
Note over U,A: SSO Registration with Domain + Approval Checks
U->>G: SSO Login (user@company.com)
G->>SSO: OAuth flow
SSO-->>G: User profile
G->>G: Check trusted domains (if configured)
alt Domain not trusted
G-->>U: Access denied
else Domain trusted
G->>G: Require admin approval?
alt Approval required (SSO_REQUIRE_ADMIN_APPROVAL=true)
G->>DB: Create PendingUserApproval
G-->>A: Approval required
A->>G: Approve user (admin UI)
Note over U,G: User logs in again after approval
U->>G: SSO Login (retry)
else Approval not required
Note right of G: Continue auto-create flow
end
G->>G: Evaluate admin criteria
Note right of G: Auto-admin domains / provider admin groups
G->>DB: Create user (is_admin true/false)
G-->>U: JWT token issued
end Password ManagementΒΆ
ContextForge now supports:
- Self-service forgot-password/reset flow:
POST /auth/email/forgot-passwordGET /auth/email/reset-password/{token}POST /auth/email/reset-password/{token}- Admin unlock flow:
POST /auth/email/admin/users/{email}/unlock- Admin password reset via UI and API (
PUT /auth/email/admin/users/{email})
Operational guidance, Kubernetes recovery commands, emergency SQL procedures, and SMTP/reset configuration are documented in:
Password Security RequirementsΒΆ
- Argon2id hashing for stored credentials
- One-time reset tokens (hashed in DB, configurable expiry)
- Password reset request rate limiting
- Failed login tracking and account lockout controls
- Audit events for login/reset/lockout/unlock actions
Role-Based UI ExperienceΒΆ
The user interface adapts based on the user's assigned roles:
Platform Admin ExperienceΒΆ
- Full System Access: Can view and manage all teams, users, and resources across the platform
- Global Configuration: Access to system-wide settings, SSO configuration, and platform management
- Cross-Team Management: Can manage resources in any team regardless of membership
- User Management: Can create, modify, and delete user accounts and role assignments
Team Admin ExperienceΒΆ
- Team Management: Can modify team settings, manage team membership (invite/remove members)
- Resource Control: Full access to create, modify, and delete team resources
- Member Oversight: Can view and manage all team members and their access
- Limited to Assigned Teams: Only sees teams where they have the team_admin role
Developer ExperienceΒΆ
- Tool Access: Can view and execute tools within their teams
- Resource Usage: Can access and use team resources and prompts
- No Management Rights: Cannot manage team membership or team settings
- Create Resources: Can create new tools, resources, and prompts within their teams
Viewer ExperienceΒΆ
- Read and Execute Access: Can view tools, resources, and prompts and execute tools within team scope
- No Creation Rights: Cannot create new resources or tools
- No Management Access: Cannot manage team membership or settings
- Limited Mutation: Can execute tools but cannot create, update, or delete resources
Default Visibility & SharingΒΆ
- Default on create: New resources (including MCP Servers, Tools, Resources, Prompts, and A2A Agents) default to
visibility="private"unless a different value is explicitly provided by an allowed actor. For servers created via the UI, the visibility is enforced toprivateby default. - Team assignment: When a user creates a server and does not specify
team_id, the server is automatically assigned to the user's personal team. -
Sharing workflow:
-
Private β Team: Make the resource visible to the owning team by setting
visibility="team". - Private/Team β Public: Make the resource visible to all authenticated users by setting
visibility="public". - Cross-team: To have a resource under a different team, create it in that team or move/clone it per policy; cross-team "share" is by visibility, not multi-team ownership.
Complete Multi-Tenancy FlowΒΆ
End-to-End Resource AccessΒΆ
sequenceDiagram
participant U as User
participant G as Gateway
participant Auth as Authentication
participant Team as Team Service
participant Res as Resource Service
participant DB as Database
Note over U,DB: Complete Access Flow
U->>G: Request resource list
G->>Auth: Validate JWT token
Auth-->>G: User identity confirmed
G->>Team: Get user teams
Team->>DB: Query team memberships
DB-->>Team: User team list
Team-->>G: Teams with roles
G->>Res: List resources for user
Res->>DB: Query with team filtering
Note right of Res: WHERE owner_email = user OR team_id IN user_teams AND visibility IN team,public OR visibility = public
DB-->>Res: Filtered resource list
Res-->>G: User-accessible resources
G-->>U: Resource list response
Note over U,DB: Resource Creation
U->>G: Create new resource
G->>Auth: Validate permissions
G->>Team: Verify team membership
Team-->>G: Team access confirmed
G->>Res: Create resource
Res->>DB: INSERT with team_id, owner_email, visibility
DB-->>Res: Resource created
Res-->>G: Creation confirmed
G-->>U: Resource created successfully Team-Based Resource FilteringΒΆ
flowchart TD
A[User Request] --> B[Extract User Identity]
B --> C[Get User Team Memberships]
C --> D[Build Filter Criteria]
D --> E{Resource Query}
E --> F["Owner-Owned Resources
owner_email = user.email"]
E --> G["Team Resources
team_id IN user.teams
AND visibility IN team,public"]
E --> H["Public Resources
visibility = public"]
F --> I[Combine Results]
G --> I
H --> I
I --> J[Apply Additional Filters]
J --> K[Return Filtered Resources]
subgraph "Filter Logic"
L[Personal: User owns directly]
M[Team: User is team member]
N[Public: Available to all]
end
style F fill:#e1f5fe
style G fill:#e3f2fd
style H fill:#e8f5e8 Database Schema DesignΒΆ
Core Multi-Tenant Schema (Key Fields)ΒΆ
erDiagram
%% User Management
EmailUser ||--o{ EmailTeamMember : belongs_to
EmailUser ||--o{ EmailTeamInvitation : invites
EmailUser ||--o{ EmailTeamJoinRequest : requests
EmailUser ||--o{ EmailTeam : creates
%% Team Management
EmailTeam ||--o{ EmailTeamMember : has
EmailTeam ||--o{ EmailTeamInvitation : has_pending
EmailTeam ||--o{ EmailTeamJoinRequest : has_requests
EmailTeam ||--o{ Tool : owns
EmailTeam ||--o{ Server : owns
EmailTeam ||--o{ Resource : owns
EmailTeam ||--o{ Prompt : owns
EmailTeam ||--o{ A2AAgent : owns
EmailUser {
string email PK
string full_name
boolean is_admin
boolean is_active
string auth_provider
timestamp created_at
timestamp updated_at
}
EmailTeam {
string id PK
string name
string slug
text description
string created_by FK
boolean is_personal
string visibility
int max_members
boolean is_active
timestamp created_at
timestamp updated_at
}
EmailTeamMember {
string id PK
string team_id FK
string user_email FK
string role
string invited_by FK
timestamp joined_at
boolean is_active
}
EmailTeamInvitation {
string id PK
string team_id FK
string email
string role
string invited_by FK
timestamp invited_at
string token
timestamp expires_at
boolean is_active
}
EmailTeamJoinRequest {
string id PK
string team_id FK
string user_email FK
string status
timestamp requested_at
timestamp expires_at
timestamp reviewed_at
string reviewed_by FK
string notes
}
Tool {
string id PK
string original_name
string display_name
string team_id FK
string owner_email
string visibility
timestamp created_at
timestamp updated_at
}
Resource {
string id PK
string uri
string name
string team_id FK
string owner_email
string visibility
timestamp created_at
timestamp updated_at
}
Prompt {
string id PK
string name
string original_name
string team_id FK
string owner_email
string visibility
timestamp created_at
timestamp updated_at
}
A2AAgent {
string id PK
string name
string slug
string team_id FK
string owner_email
string visibility
timestamp created_at
timestamp updated_at
}
Server {
string id PK
string name
string team_id FK
string owner_email
string visibility
timestamp created_at
timestamp updated_at
} API Design PatternsΒΆ
Team-Scoped EndpointsΒΆ
All resource endpoints follow consistent team-scoping patterns:
flowchart TD
subgraph "API Endpoint Patterns"
A["GET /tools?team_id=uuid&visibility=team"]
B["POST /tools
name, team_id (optional), visibility"]
C["GET /tools/{tool_id}"]
D["PUT /tools/{tool_id}
visibility, name, etc."]
E["DELETE /tools/{tool_id}"]
end
subgraph "Request Processing"
F[Extract User Identity] --> G[Validate Team Access]
G --> H[Apply Team Filters]
H --> I[Execute Query]
I --> J[Return Results]
end
subgraph "Access Control Checks"
K[User Team Membership]
L[Resource Ownership]
M[Visibility Level]
N[Operation Permissions]
end
A --> F
B --> F
C --> F
D --> F
E --> F
G --> K
G --> L
G --> M
G --> N
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#fff3e0
style D fill:#e8f5e8
style E fill:#ffebee Resource Creation FlowΒΆ
sequenceDiagram
participant C as Client
participant G as Gateway
participant A as Auth Middleware
participant R as Resource Service
participant DB as Database
C->>G: POST /tools
Note right of C: {name, team_id, visibility}
G->>A: Validate request
A->>A: Extract user from JWT
A->>A: Enforce permissions + token scope
A-->>G: User authorized
G->>R: Create resource
R->>R: Validate payload + defaults
R->>DB: INSERT resource
Note right of R: team_id, owner_email, visibility
DB-->>R: Resource created
R-->>G: Creation response
G-->>C: 201 Created Configuration & EnvironmentΒΆ
Multi-Tenancy ConfigurationΒΆ
#####################################
# Multi-Tenancy Configuration
#####################################
# Team Settings
AUTO_CREATE_PERSONAL_TEAMS=true
# PERSONAL_TEAM_PREFIX=personal # optional: set to get collision-safe email-based slugs
MAX_TEAMS_PER_USER=50
MAX_MEMBERS_PER_TEAM=100 # default limit resolved at check time; platform admins are exempt
# Team Invitation Settings
INVITATION_EXPIRY_DAYS=7
REQUIRE_EMAIL_VERIFICATION_FOR_INVITES=true
# Team Governance
ALLOW_TEAM_CREATION=true
ALLOW_TEAM_JOIN_REQUESTS=true
ALLOW_TEAM_INVITATIONS=true
# Visibility
# NOTE: Resources default to 'private' (not configurable via env today)
# Allowed visibility values: private | team | public
# Platform Administration
PLATFORM_ADMIN_EMAIL=admin@company.com
PLATFORM_ADMIN_PASSWORD=changeme
PLATFORM_ADMIN_FULL_NAME="Platform Administrator"
# SSO (enable + trust and admin mapping)
SSO_ENABLED=true
SSO_TRUSTED_DOMAINS=["company.com","trusted-partner.com"]
SSO_AUTO_ADMIN_DOMAINS=["company.com"]
SSO_GITHUB_ADMIN_ORGS=["your-org"]
SSO_GOOGLE_ADMIN_DOMAINS=["your-google-workspace-domain.com"]
SSO_REQUIRE_ADMIN_APPROVAL=false
# Public team self-join flows are planned; no env toggles yet
Security ConsiderationsΒΆ
Multi-Tenant Security ModelΒΆ
flowchart TD
subgraph "Security Layers"
A["Authentication Layer
- JWT validation
- Session management"]
B["Authorization Layer
- Team membership
- Resource ownership
- Visibility checks"]
C["Data Isolation Layer
- Team-scoped queries
- Owner validation
- Access logging"]
end
subgraph "Security Controls"
D["Input Validation
- Team ID validation
- Email format
- Role validation"]
E["Rate Limiting
- Per-user limits
- Per-team limits
- API quotas"]
F["Audit Logging
- Access attempts
- Resource changes
- Team modifications"]
end
subgraph "Attack Prevention"
G["Team Enumeration
- UUID team IDs
- Access validation"]
H["Resource Access
- Ownership checks
- Visibility enforcement"]
I["Privilege Escalation
- Role validation
- Permission boundaries"]
end
A --> B --> C
D --> E --> F
G --> H --> I
style A fill:#ffcdd2
style B fill:#f8bbd9
style C fill:#e1bee7
style D fill:#c8e6c9
style E fill:#dcedc8
style F fill:#f0f4c3 RBAC Access Control MatrixΒΆ
| RBAC Role | Scope | Team Access | Resource Creation | Member Management | Team Settings | Platform Admin |
|---|---|---|---|---|---|---|
| Platform Admin | Global | All teams | All resources | All teams | All settings | Full access |
| Team Admin | Team | Assigned teams | Team resources | Team members | Team settings | No access |
| Developer | Team | Member teams | Team resources | No access | No access | No access |
| Viewer | Team | Member teams | No access | No access | No access | No access |
Note: Team Owner/Member roles from the team management system work alongside RBAC roles. A user can have both team membership status (Owner/Member) and RBAC role assignments (Team Admin/Developer/Viewer) within the same team.
Token Scoping InvariantsΒΆ
These behaviors are enforced consistently across all access paths:
normalize_token_teams()is the canonical interpreter of JWT team claims;resolve_session_teams()is the single policy point for session tokens (always DB-resolved)- For API/legacy tokens: missing
teamskey always returns[](public-only, secure default); emptyteams: []also returns[]. For session tokens: missing, null, or emptyteamsreturns the full DB membership (no narrowing requested) - Admin bypass for API/legacy tokens requires BOTH
teams: nullANDis_admin: true; for session tokens, admin bypass is DB-derived (is_adminflag). In both cases the service layer requirestoken_teams=NoneANDuser_email=Nonefor unrestricted queries - All list endpoints pass
token_teamsto the service layer - Service layer applies visibility filtering based on
token_teamsviaBaseService._apply_access_control() - Public-only tokens can ONLY access
visibility='public'resources β owner and team access are both suppressed - Owner-based access (
owner_email) grants visibility only forvisibility='private'resources β it does not bypass team scoping for team-visibility resources
Related DocumentationΒΆ
For detailed information on RBAC configuration, token scoping, and permission management, see RBAC Configuration.
Implementation VerificationΒΆ
Key Requirements ChecklistΒΆ
- User Authentication: Email and SSO authentication implemented
- Personal Teams: Auto-created for every user
- Team Roles: Owner and Member roles (platform Admin is global)
- Team Visibility: Private and Public team types
- Resource Scoping: All resources scoped to teams with visibility controls
- Invitation System: Email-based invitations with token management
- Platform Administration: Separate admin role with domain restrictions
- Access Control: Team-based filtering for all resources
- Database Design: Complete multi-tenant schema
- API Patterns: Consistent team-scoped endpoints
Critical Implementation PointsΒΆ
- Team ID Validation: Every resource operation must validate team membership
- Visibility Enforcement: Resource visibility (private/team/public) strictly enforced; team visibility (private/public) per design
- Owner Permissions: Only team owners can manage members and settings
- Personal Team Protection: Personal teams cannot be deleted or transferred
- Invitation Security: Invitation tokens with expiration and single-use
- Platform Admin Isolation: Platform admin access separate from team access
- Cross-Team Access: Public resources accessible across team boundaries
- Audit Trail: Permission checks and auth events audited; extended operation audit planned
Gaps & IssuesΒΆ
- Team roles: Owner and Member only (platform Admin is global) β consistent across ERD, APIs, and UI.
- Team visibility: Private and Public.
- Resource visibility:
private|team|publicβ enforced as designed. - Public team discovery/join: Joinβrequest/selfβjoin flows to be implemented.
- Default resource visibility: Defaults to "private"; not configurable via env.
- SSO admin mapping: Domain/org lists supported; providerβspecific org checks may require provider API calls in production.
Enhancements & Roadmap (Part of the Design)ΒΆ
-
Public Team Discovery & Join Requests:
-
Add endpoints and UI to request membership on public teams; owner approval workflow; optional autoβapprove policy.
-
Admin toggles/policies to restrict who can create public teams and who can approve joins.
-
Unified Operation Audit:
-
Systemβwide audit log for create/update/delete across teams, tools, servers, resources, prompts, agents with export/reporting.
-
Role Automation:
-
Autoβassign default RBAC roles on resource creation (e.g., owner gets manager role in team scope; members get viewer).
-
Optional perβteam policies defining who may create public resources.
-
ABAC for Virtual Servers:
-
Attributeβbased conditions layered on top of RBAC (tenant tags, data classifications, environment, time windows, client IP).
-
Team/Resource Quotas and Policies:
-
Perβteam limits (tools/servers/resources/agents); perβteam defaults for resource visibility and creation rights.
-
Public Resource Access Controls:
-
Fineβgrained crossβtenant rate limits and optβin masking for metadata shown to nonβmembers.
This architecture provides a robust, secure, and scalable multi-tenant system that supports complex organizational structures while maintaining strict data isolation and flexible resource sharing capabilities.