-
Notifications
You must be signed in to change notification settings - Fork 614
[TESTING][SECURITY]: RBAC manual test plan (visibility, teams, token scope) #2388
Description
🔒 Epic: RBAC Manual Test Plan (Visibility, Teams, Token Scope)
Goal
Produce a comprehensive manual RBAC test plan that documents the full access-control flow across resources, teams, and token types. The plan serves as the canonical reference for expected behavior and provides the baseline for automated regression coverage.
Why Now?
RBAC behavior spans middleware, service-layer filtering, and token scoping. A manual plan is needed to:
- End-to-End Validation: Validate the complete flow from authentication → token parsing → RBAC → visibility filtering in real environments
- Human-Readable Specification: Provide a canonical reference for behavior, troubleshooting, and onboarding
- Release Gate: Enable consistent validation of Release 1.0.0-RC1 candidates
- Regression Baseline: Serve as the specification for automated test coverage
📖 User Stories
US-1: QA Lead - Complete End-to-End Manual Coverage
As a QA Lead
I want a comprehensive manual test plan covering all RBAC flows
So that release candidates can be validated quickly and consistently
Acceptance Criteria:
Feature: RBAC Manual Test Plan Coverage
Scenario: Test plan provides complete resource coverage
Given the manual test plan document
When I review the coverage matrix
Then I should see test cases for all resource types:
| Resource Type |
| Tools |
| Resources |
| Prompts |
| Servers |
| Gateways |
| A2A Agents |
And each resource type should have visibility tests (private, team, public)
And each test case should have clear preconditions
And each test case should have step-by-step procedures
And each test case should have expected results
Scenario: Test plan is reproducible by any tester
Given a tester with database access
When the tester follows the setup instructions
Then the preconditions should be established within 15 minutes
And each test case should be executable independently
And results should be deterministic and repeatableTechnical Requirements:
- Clear preconditions and environment setup steps
- Step-by-step procedures with curl/API examples
- Expected results with HTTP status codes and response bodies
- Coverage across tools/resources/prompts/servers/gateways/A2A agents
- Rollback/cleanup procedures for each test
US-2: Security Engineer - Token Types & Scope Behavior
As a Security Engineer
I want explicit manual tests for admin, team-scoped, and public-only tokens
So that privilege boundaries are validated before each release
Acceptance Criteria:
Feature: Token Type Access Control
Scenario Outline: Admin token grants unrestricted access
Given an admin token with is_admin=true and teams=null
And a <visibility> resource owned by another user
When I access the resource with the admin token
Then the request should return status <status>
And the response should contain the resource data
Examples:
| visibility | status |
| private | 200 |
| team | 200 |
| public | 200 |
Scenario: Team-scoped token restricts to team resources
Given a team-scoped token with teams=["team-alpha"]
And a team-visible resource in "team-alpha"
And a team-visible resource in "team-beta"
When I list resources with the team-scoped token
Then I should see the "team-alpha" resource
And I should NOT see the "team-beta" resource
Scenario: Public-only token cannot access private resources
Given a public-only token with teams=[]
And a private resource owned by the token's user
When I access the private resource
Then the request should return status 403
And the response should contain "Access denied"
Scenario: Public-only token can access public resources
Given a public-only token with teams=[]
And a public resource in any team
When I access the public resource
Then the request should return status 200Technical Requirements:
- Admin token bypass confirmed for all visibility levels
- Team-scoped token restricted to specified teams only
- Public-only token cannot access private resources (even if owner)
- Token claims verified:
is_admin,teams,sub(user email) - Error messages do not leak internal details
US-3: MCP Operator - Permissive Mode Exception
As an MCP Operator
I want manual tests for permissive vs strict MCP authentication modes
So that public-only unauthenticated access is validated correctly
Acceptance Criteria:
Feature: MCP Authentication Modes
Scenario: Permissive mode allows unauthenticated public access
Given MCP_REQUIRE_AUTH=false (permissive mode)
And a public tool exists
And a team-visible tool exists
When I invoke tools/list without authentication
Then I should receive the public tool
And I should NOT receive the team-visible tool
And the response should NOT contain private tools
Scenario: Permissive mode blocks unauthenticated team access
Given MCP_REQUIRE_AUTH=false (permissive mode)
And a team-visible resource exists
When I attempt to access the team resource without authentication
Then the request should return status 403
And the response should indicate "Authentication required for team resources"
Scenario: Strict mode blocks all unauthenticated access
Given MCP_REQUIRE_AUTH=true (strict mode)
And a public tool exists
When I invoke tools/list without authentication
Then the request should return status 401
And the response should indicate "Authentication required"
Scenario: Authenticated user in permissive mode gets full RBAC
Given MCP_REQUIRE_AUTH=false (permissive mode)
And a valid JWT token for a team member
When I invoke tools/list with the token
Then I should receive public tools
And I should receive team-visible tools for my teams
And I should receive my private toolsTechnical Requirements:
- Permissive mode: unauthenticated → public-only (no owner/team access)
- Strict mode: unauthenticated → 401 for all endpoints
- Authenticated requests follow full RBAC regardless of mode
- Configuration toggle:
MCP_REQUIRE_AUTHenvironment variable - Test both SSE and WebSocket transports
US-4: Developer - Visibility and Ownership Semantics
As a Developer
I want tests that validate visibility vs ownership distinctions
So that I understand the correct access behavior
Acceptance Criteria:
Feature: Visibility vs Ownership
Scenario: Private visibility restricts to owner only
Given User A owns a private resource in Team T
And User B is a member of Team T
When User B attempts to access the private resource
Then the request should return status 403
And only User A should be able to access the resource
Scenario: Team visibility allows all team members
Given User A owns a team-visible resource in Team T
And User B is a member of Team T
And User C is NOT a member of Team T
When User B accesses the resource
Then the request should return status 200
When User C attempts to access the resource
Then the request should return status 403
Scenario: Public visibility allows all authenticated users
Given User A owns a public resource in Team T
And User D is authenticated but not in Team T
When User D accesses the resource
Then the request should return status 200
And the response should contain the resource data
Scenario: Resource belongs to team but visibility controls access
Given a public resource belongs to Team T (team_id=T)
And User B is NOT a member of Team T
When User B accesses the resource
Then the request should return status 200
And the resource's team_id should still show Team TTechnical Requirements:
owner_email→ who created the resourceteam_id→ organizational ownership (where it belongs)visibility→ who can access (private/team/public)- Private: owner only (even team members excluded)
- Team: all team members regardless of who owns it
- Public: all authenticated users
US-5: Platform Admin - Team Filter Semantics
As a Platform Administrator
I want tests that validate team filtering behavior on list endpoints
So that users see only resources belonging to the filtered team
Acceptance Criteria:
Feature: Team Filter Behavior
Scenario: Team filter shows only resources belonging to that team
Given User A is a member of Team T and Team X
And Team T has: Tool-T1 (team), Tool-T2 (public)
And Team X has: Tool-X1 (public)
When User A lists tools with team_id=T
Then the response should contain Tool-T1 and Tool-T2
And the response should NOT contain Tool-X1
And public resources from other teams are excluded
Scenario: Team filter respects visibility within the team
Given User A is a member of Team T
And Team T has:
| Resource | Visibility | Owner |
| Private-A | private | User A |
| Private-B | private | User B |
| Team-Tool | team | User B |
| Public-Tool | public | User B |
When User A lists tools with team_id=T
Then the response should contain: Private-A, Team-Tool, Public-Tool
And the response should NOT contain: Private-B
Scenario: Non-member cannot filter by team they don't belong to
Given User C is NOT a member of Team T
When User C lists resources with team_id=T
Then the response should return an empty list
And no resources from Team T should be visibleTechnical Requirements:
?team_id=Xfilter restricts to resources withteam_id=X- Public resources from OTHER teams are excluded when filtering
- Visibility rules still apply within the filtered team
- Non-members get empty results (not an error)
US-6: Security Auditor - Token Scope Intersection
As a Security Auditor
I want tests that verify token scopes intersect with RBAC permissions
So that tokens cannot grant more access than RBAC allows
Acceptance Criteria:
Feature: Token Scope Intersection with RBAC
Scenario: Token permissions restrict RBAC grants
Given RBAC grants User A tools.execute permission
And User A's token has permissions=["tools.read"]
When User A attempts to execute a tool
Then the request should return status 403
And the denial should cite "Token scope insufficient"
Scenario: RBAC denial overrides token grants
Given RBAC denies User B prompts.delete permission
And User B's token has permissions=["prompts.*"]
When User B attempts to delete a prompt
Then the request should return status 403
And the denial should cite "Permission denied"
Scenario: Token without explicit permissions inherits RBAC
Given RBAC grants User A resources.read permission
And User A's token has permissions=null (inherit)
When User A reads a resource
Then the request should return status 200
Scenario: Token server_id scope limits access
Given a token scoped to server_id="server-1"
And Tool A belongs to server-1
And Tool B belongs to server-2
When the token holder lists tools
Then the response should contain Tool A
And the response should NOT contain Tool BTechnical Requirements:
- Token
permissionsfield intersects with RBAC-granted permissions - Token cannot expand access beyond what RBAC grants
- RBAC denial always wins regardless of token claims
server_idscope restricts to specific server's resources- Future: IP restrictions, time windows, usage limits
US-7: Release Manager - Ownership CRUD Validation
As a Release Manager
I want tests that validate ownership rules for create/update/delete
So that users can only modify resources they're authorized to change
Acceptance Criteria:
Feature: Ownership CRUD Rules
Scenario: Owner can update their private resource
Given User A owns a private tool
When User A updates the tool description
Then the request should return status 200
And the tool should reflect the updated description
Scenario: Owner can delete their private resource
Given User A owns a private prompt
When User A deletes the prompt
Then the request should return status 200
And the prompt should no longer exist
Scenario: Non-owner cannot update private resource
Given User A owns a private resource in Team T
And User B is a member of Team T (not owner)
When User B attempts to update the resource
Then the request should return status 403
Scenario: Team owner can update team-visible resources
Given User A is an owner of Team T
And User B created a team-visible tool in Team T
When User A updates the tool
Then the request should return status 200
And team owners have management rights over team resources
Scenario: Team member can update their own creations
Given User B is a member (not owner) of Team T
And User B created a team-visible resource
When User B updates their resource
Then the request should return status 200Technical Requirements:
- Private: only owner can update/delete
- Team (owner): full CRUD on all team resources
- Team (member): CRUD on their own creations
- Public: same rules as team visibility for modifications
- Delete operations respect the same ownership rules
🏗 Test Architecture
Access Control Decision Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ ACCESS CONTROL DECISION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐
│ User Request │
└──────┬───────┘
│
┌──────▼───────┐
│ Token Valid? │
└──────┬───────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ Admin │ │ Team-Scoped │ │ Public │
│ (is_admin │ │ (teams=[T]) │ │ Only │
│ +null) │ │ │ │ (teams=[])│
└─────┬─────┘ └──────┬──────┘ └─────┬─────┘
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ ALLOW ALL │ │ Check │ │ ONLY │
│ │ │ Visibility │ │ PUBLIC │
└───────────┘ └──────┬──────┘ └───────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ public │ │ team │ │ private │
│ ALLOW │ │ team_id in │ │ owner == │
│ │ │ token.teams?│ │ user? │
└───────────┘ └─────────────┘ └───────────┘
Token Scope Intersection
┌─────────────────────────────────────────────────────────────────────────────┐
│ TOKEN SCOPE + RBAC INTERSECTION │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────┐
│ Request: resource.action │
└──────────────┬────────────┘
│
┌──────────────▼────────────┐
│ RBAC grants permission? │
└──────────────┬────────────┘
│
┌──────────┴──────────┐
│ │
NO ──► DENY YES (continue)
│
┌───────────▼───────────┐
│ Token scope allows? │
│ (permissions, server, │
│ IP, time, usage) │
└───────────┬───────────┘
│
┌──────────┴──────────┐
│ │
NO ──► DENY YES ──► ALLOW
List Endpoint with Team Filter
┌─────────────────────────────────────────────────────────────────────────────┐
│ LIST ENDPOINT (team_id filter) │
└─────────────────────────────────────────────────────────────────────────────┘
GET /resources?team_id=T
│
┌─────────────▼─────────────┐
│ User member of Team T? │
└─────────────┬─────────────┘
│
┌───────────────────┼───────────────────┐
│ NO │ YES
┌─────▼─────┐ ┌──────▼──────┐
│ Return [] │ │ Filter by │
│ (empty) │ │ team_id = T │
└───────────┘ └──────┬──────┘
│
┌───────────────────────────┼───────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ visibility= │ │ visibility= │ │ visibility= │
│ private │ │ team │ │ public │
│ owner=user? │ │ INCLUDE │ │ INCLUDE │
└──────┬──────┘ └─────────────┘ └─────────────┘
│
┌───────────┼───────────┐
│ YES │ │ NO
┌─────▼─────┐ ┌─────▼─────┐
│ INCLUDE │ │ EXCLUDE │
└───────────┘ └───────────┘
NOTE: Resources with team_id != T are NEVER included, even if public.
📋 Test Environment Setup
Preconditions
Before executing tests, establish the following environment:
1. Users
| User ID | Is Admin | Teams | |
|---|---|---|---|
| User A | alice@example.com | No | Team T (owner), Team X (member) |
| User B | bob@example.com | No | Team X (owner) |
| User C | charlie@example.com | No | (none) |
| Admin | admin@example.com | Yes | (admin bypass) |
2. Teams
| Team ID | Name | Visibility |
|---|---|---|
| Team T | team-alpha | private |
| Team X | team-beta | public |
3. Test Resources (Created by User A in Team T)
Create one resource of each type for each visibility level:
| Resource Type | Private | Team | Public |
|---|---|---|---|
| Tools | tool-priv-a |
tool-team-a |
tool-pub-a |
| Resources | res-priv-a |
res-team-a |
res-pub-a |
| Prompts | prompt-priv-a |
prompt-team-a |
prompt-pub-a |
| Servers | srv-priv-a |
srv-team-a |
srv-pub-a |
| Gateways | gw-priv-a |
gw-team-a |
gw-pub-a |
| A2A Agents | a2a-priv-a |
a2a-team-a |
a2a-pub-a |
4. Token Types
Generate the following tokens for testing:
# Admin token (is_admin=true, teams=null)
export ADMIN_TOKEN=$(python -m mcpgateway.utils.create_jwt_token \
--username admin@example.com --is-admin --exp 60 --secret $JWT_SECRET_KEY)
# Team-scoped token for User A (teams=["team-alpha"])
export TEAM_A_TOKEN=$(python -m mcpgateway.utils.create_jwt_token \
--username alice@example.com --teams '["team-alpha"]' --exp 60 --secret $JWT_SECRET_KEY)
# Public-only token for User A (teams=[])
export PUBLIC_ONLY_TOKEN=$(python -m mcpgateway.utils.create_jwt_token \
--username alice@example.com --teams '[]' --exp 60 --secret $JWT_SECRET_KEY)
# User B token (team-beta member)
export USER_B_TOKEN=$(python -m mcpgateway.utils.create_jwt_token \
--username bob@example.com --teams '["team-beta"]' --exp 60 --secret $JWT_SECRET_KEY)
# User C token (no teams)
export USER_C_TOKEN=$(python -m mcpgateway.utils.create_jwt_token \
--username charlie@example.com --teams '[]' --exp 60 --secret $JWT_SECRET_KEY)5. Environment Configuration
# For permissive mode tests
export MCP_REQUIRE_AUTH=false
# For strict mode tests
export MCP_REQUIRE_AUTH=true🧪 Manual Test Cases
Section 1: Visibility Access (Authenticated)
| Case | Auth | Token | Resource | Visibility | Owner | Expected | HTTP Status |
|---|---|---|---|---|---|---|---|
| V-01 | User B | team-scoped | tool-pub-a | public | User A | ALLOW | 200 |
| V-02 | User B | team-scoped | tool-team-a | team | User A | DENY | 403 |
| V-03 | User B | team-scoped | tool-priv-a | private | User A | DENY | 403 |
| V-04 | User A | team-scoped | tool-priv-a | private | User A | ALLOW | 200 |
| V-05 | User A | team-scoped | tool-team-a | team | User A | ALLOW | 200 |
| V-06 | User C | public-only | tool-pub-a | public | User A | ALLOW | 200 |
| V-07 | User C | public-only | tool-team-a | team | User A | DENY | 403 |
| V-08 | User C | public-only | tool-priv-a | private | User A | DENY | 403 |
V-01: Access public resource as non-owner
Preconditions:
- User B authenticated with team-scoped token (team-beta)
tool-pub-aexists with visibility=public in Team T
Steps:
curl -X GET "http://localhost:4444/tools/tool-pub-a" \
-H "Authorization: Bearer $USER_B_TOKEN"Expected Result:
- HTTP 200 OK
- Response body contains tool details
- Public resources are accessible to all authenticated users
V-02: Deny team resource access to non-member
Preconditions:
- User B authenticated with team-scoped token (team-beta)
tool-team-aexists with visibility=team in Team T- User B is NOT a member of Team T
Steps:
curl -X GET "http://localhost:4444/tools/tool-team-a" \
-H "Authorization: Bearer $USER_B_TOKEN"Expected Result:
- HTTP 403 Forbidden
- Response:
{"detail": "Access denied"} - Team resources restricted to team members
V-03: Deny private resource access to non-owner
Preconditions:
- User B authenticated
tool-priv-aexists with visibility=private, owner=User A
Steps:
curl -X GET "http://localhost:4444/tools/tool-priv-a" \
-H "Authorization: Bearer $USER_B_TOKEN"Expected Result:
- HTTP 403 Forbidden
- Private resources only accessible by owner
Section 2: Token Type Behavior
| Case | Token Type | is_admin | teams claim | Access Level | Expected |
|---|---|---|---|---|---|
| T-01 | Admin | true | null | All resources | ALLOW ALL |
| T-02 | Team-Scoped | false | ["team-alpha"] | Team T + public + owned | ALLOW SCOPED |
| T-03 | Public-Only | false | [] | Public only | ALLOW PUBLIC |
| T-04 | Public-Only | false | [] | Private (even if owner) | DENY |
T-01: Admin token bypasses all visibility checks
Preconditions:
- Admin token with is_admin=true, teams=null
- Private resources from multiple users exist
Steps:
# Access User A's private tool
curl -X GET "http://localhost:4444/tools/tool-priv-a" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Access User B's private resource (if exists)
curl -X GET "http://localhost:4444/resources" \
-H "Authorization: Bearer $ADMIN_TOKEN"Expected Result:
- HTTP 200 OK for all requests
- Admin can access any resource regardless of visibility
- Response includes all resources
T-04: Public-only token denies owner's private resources
Preconditions:
- User A's public-only token (teams=[])
- User A owns
tool-priv-a(private)
Steps:
curl -X GET "http://localhost:4444/tools/tool-priv-a" \
-H "Authorization: Bearer $PUBLIC_ONLY_TOKEN"Expected Result:
- HTTP 403 Forbidden
- Public-only tokens cannot access private resources
- Even ownership doesn't override teams=[] restriction
Section 3: Team Filter Semantics
| Case | User | Filter | Resource Team | Visibility | Expected |
|---|---|---|---|---|---|
| F-01 | User A | team_id=T | Team T | team | LIST |
| F-02 | User A | team_id=T | Team T | public | LIST |
| F-03 | User A | team_id=T | Team T | private (owner) | LIST |
| F-04 | User A | team_id=T | Team T | private (not owner) | DROP |
| F-05 | User A | team_id=T | Team X | public | DROP |
| F-06 | User B | team_id=T | Team T | public | DROP (not member) |
F-05: Public resources from other teams excluded when filtering
Preconditions:
- User A is member of Team T and Team X
tool-pub-xexists with visibility=public in Team X- User A lists with team_id=T filter
Steps:
curl -X GET "http://localhost:4444/tools?team_id=team-alpha" \
-H "Authorization: Bearer $TEAM_A_TOKEN"Expected Result:
- Response contains only Team T resources
tool-pub-xis NOT included (belongs to Team X)- Team filter restricts to resources BELONGING to that team
Section 4: Token Scope Intersection
| Case | RBAC Grants | Token Permissions | Expected |
|---|---|---|---|
| S-01 | tools.execute | null (inherit) | ALLOW |
| S-02 | tools.execute | ["tools.read"] | DENY (scope restricts) |
| S-03 | (no grant) | ["tools.execute"] | DENY (RBAC wins) |
| S-04 | resources.read | ["resources.*"] | ALLOW |
S-02: Token scope restricts RBAC-granted permission
Preconditions:
- User A has RBAC permission
tools.execute - Token created with
permissions=["tools.read"] - A tool exists that User A owns
Steps:
# Create token with restricted scope
RESTRICTED_TOKEN=$(python -m mcpgateway.utils.create_jwt_token \
--username alice@example.com \
--teams '["team-alpha"]' \
--permissions '["tools.read"]' \
--exp 60 --secret $JWT_SECRET_KEY)
# Attempt to execute tool
curl -X POST "http://localhost:4444/tools/tool-pub-a/invoke" \
-H "Authorization: Bearer $RESTRICTED_TOKEN" \
-H "Content-Type: application/json" \
-d '{"arguments": {}}'Expected Result:
- HTTP 403 Forbidden
- Response:
{"detail": "Token scope insufficient"} - Token permissions=["tools.read"] doesn't include tools.execute
Section 5: MCP Permissive Mode
| Case | MCP_REQUIRE_AUTH | Auth | Resource Visibility | Expected |
|---|---|---|---|---|
| M-01 | false | none | public | ALLOW |
| M-02 | false | none | team | DENY |
| M-03 | false | none | private | DENY |
| M-04 | true | none | public | DENY (401) |
| M-05 | false | valid | team (member) | ALLOW |
M-01: Unauthenticated access to public tools in permissive mode
Preconditions:
MCP_REQUIRE_AUTH=false- Server restarted with permissive mode
tool-pub-aexists with visibility=public
Steps:
# MCP tools/list without auth
curl -X POST "http://localhost:4444/mcp/sse" \
-H "Content-Type: application/json" \
-d '{"method": "tools/list", "params": {}}'Expected Result:
- Response includes
tool-pub-a - Response does NOT include team/private tools
- Unauthenticated = public-only access
M-04: Strict mode rejects unauthenticated requests
Preconditions:
MCP_REQUIRE_AUTH=true- Server restarted with strict mode
Steps:
curl -X POST "http://localhost:4444/mcp/sse" \
-H "Content-Type: application/json" \
-d '{"method": "tools/list", "params": {}}'Expected Result:
- HTTP 401 Unauthorized
- All MCP endpoints require authentication
Section 6: Ownership CRUD Operations
| Case | User | Action | Resource Owner | Resource Visibility | Expected |
|---|---|---|---|---|---|
| O-01 | User A | UPDATE | User A | private | ALLOW |
| O-02 | User A | DELETE | User A | private | ALLOW |
| O-03 | User B | UPDATE | User A | private | DENY |
| O-04 | User B | DELETE | User A | private | DENY |
| O-05 | Team Owner | UPDATE | User B | team | ALLOW |
| O-06 | Team Member | UPDATE | User B | team | DENY (not owner) |
O-03: Non-owner cannot update private resource
Preconditions:
tool-priv-aowned by User A (visibility=private)- User B authenticated
Steps:
curl -X PATCH "http://localhost:4444/tools/tool-priv-a" \
-H "Authorization: Bearer $USER_B_TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "Hacked description"}'Expected Result:
- HTTP 403 Forbidden
- Only owner can modify private resources
Section 7: All Resource Types
Repeat visibility and ownership tests for each resource type:
| Resource Type | List Endpoint | Get Endpoint | Create | Update | Delete |
|---|---|---|---|---|---|
| Tools | GET /tools | GET /tools/{id} | POST /tools | PATCH /tools/{id} | DELETE /tools/{id} |
| Resources | GET /resources | GET /resources/{id} | POST /resources | PATCH /resources/{id} | DELETE /resources/{id} |
| Prompts | GET /prompts | GET /prompts/{id} | POST /prompts | PATCH /prompts/{id} | DELETE /prompts/{id} |
| Servers | GET /servers | GET /servers/{id} | POST /servers | PATCH /servers/{id} | DELETE /servers/{id} |
| Gateways | GET /gateways | GET /gateways/{id} | POST /gateways | PATCH /gateways/{id} | DELETE /gateways/{id} |
| A2A Agents | GET /a2a/agents | GET /a2a/agents/{id} | POST /a2a/agents | PATCH /a2a/agents/{id} | DELETE /a2a/agents/{id} |
🧾 Complete Test Matrix
Access Control Matrix
| # | Auth | is_admin | Token Teams | Visibility | Resource Team | Owner Match | Expected |
|---|---|---|---|---|---|---|---|
| 1 | yes | false | [] | public | any | n/a | ALLOW |
| 2 | yes | false | [] | team | T | n/a | DENY |
| 3 | yes | false | [] | private | T | yes | DENY |
| 4 | yes | false | [T] | team | T | n/a | ALLOW |
| 5 | yes | false | [T] | team | X | n/a | DENY |
| 6 | yes | false | [T] | private | T | yes | ALLOW |
| 7 | yes | false | [T] | private | T | no | DENY |
| 8 | yes | true | null | private | any | n/a | ALLOW |
| 9 | no | n/a | n/a | public | any | n/a | ALLOW (permissive) |
| 10 | no | n/a | n/a | team | any | n/a | DENY |
| 11 | no | n/a | n/a | private | any | n/a | DENY |
| 12 | no | n/a | n/a | any | any | n/a | DENY (strict) |
Team Filter Matrix
| # | Auth | Token Teams | Filter | Resource Team | Visibility | Owner | Expected |
|---|---|---|---|---|---|---|---|
| 13 | yes | [T] | T | T | team | n/a | LIST |
| 14 | yes | [T] | T | T | public | n/a | LIST |
| 15 | yes | [T] | T | T | private | yes | LIST |
| 16 | yes | [T] | T | T | private | no | DROP |
| 17 | yes | [T] | T | X | public | n/a | DROP |
| 18 | yes | [] | T | T | public | n/a | LIST |
| 19 | yes | [] | T | T | private | yes | DROP |
| 20 | yes | [X] | T | T | team | n/a | DROP (not member) |
Token Scope Matrix
| # | RBAC Grants | Token Permissions | Expected |
|---|---|---|---|
| 21 | yes | null/empty (inherit) | ALLOW |
| 22 | yes | excludes action | DENY |
| 23 | no | includes action | DENY |
| 24 | yes | includes action | ALLOW |
✅ Success Criteria
- Coverage: All 6 resource types tested with all visibility levels
- Token Types: Admin, team-scoped, and public-only tokens validated
- Team Filters: Team filter semantics verified (excludes other teams' public resources)
- Token Scopes: Intersection with RBAC confirmed (token restricts, cannot expand)
- MCP Modes: Permissive and strict authentication modes validated
- Ownership: CRUD operations respect owner/team admin rules
- Reproducibility: Any tester can execute the plan with deterministic results
- Documentation: All test cases documented with curl examples
🏁 Definition of Done
- Manual test plan reviewed by QA and Security teams
- All preconditions documented with setup scripts
- All test cases have step-by-step procedures
- All test cases have expected results with HTTP status codes
- Test matrix covers all combinations from https://ibm.github.io/mcp-context-forge/architecture/multitenancy/
- Token generation scripts provided and validated
- MCP permissive/strict mode tests included
- All 6 resource types covered (tools, resources, prompts, servers, gateways, A2A)
- Plan successfully executed against Release 1.0.0-RC1
- Results documented and any failures triaged
- Linked as prerequisite for automated regression suite (#TBD)
📋 Automation Roadmap
After manual validation, convert to automated tests:
Phase 1: Pytest Fixtures
- Create
conftest.pyfixtures for test users, teams, tokens - Create resource factory fixtures for all 6 types
- Create token generation fixtures
Phase 2: Parametrized Tests
- Convert access matrix to
@pytest.mark.parametrize - Convert team filter matrix to parametrized tests
- Convert token scope matrix to parametrized tests
Phase 3: Integration Tests
- Test against live database (SQLite/PostgreSQL)
- Test MCP SSE and WebSocket transports
- Add to CI/CD pipeline
🔗 References
- https://ibm.github.io/mcp-context-forge/architecture/multitenancy/ - RBAC model documentation
mcpgateway/middleware/- Token and auth middlewaremcpgateway/services/- Resource service layer with visibility filteringtests/- Existing test patterns
📝 Notes
🔹 Key Distinction: team_id is organizational ownership (where resource lives), visibility controls who can access. A public resource in Team T is accessible to all authenticated users, but still "belongs" to Team T.
🔹 Public-Only Token: When teams=[], the token holder can ONLY access public resources. This is true even if they are the owner of a private resource - the token scope overrides ownership.
🔹 Team Filter Semantics: When filtering by team_id=T, only resources with team_id=T are returned. Public resources from other teams are excluded from the results. This is filtering (which team's resources to show), not access control (whether user can see them).
🔹 Token Scope Intersection: Token permissions INTERSECT with RBAC grants. If RBAC grants tools.execute but token only has ["tools.read"], execute is denied. If token has ["tools.execute"] but RBAC doesn't grant it, execute is still denied.
📋 Implementation References (Verified)
Visibility Model Clarification:
- In permissive mode (
MCP_REQUIRE_AUTH=false), unauthenticated/mcprequests getteams=[]- Services use
token_teams=[]to restrict to public resources only- Evidence:
streamablehttp_transport.py:1367-1373,tool_service.py:1744-1758
Admin Token Nuance:
- Admin token bypass (
is_admin=true, teams=null) works when the teams claim is missing or null- Tokens created via the Token Catalog always set a list (
teams=[]or specific team IDs)- Evidence:
streamablehttp_transport.py:1242-1258,token_catalog_service.py:266-268- Test expectations should account for token shapes that yield unrestricted access