Skip to content

fix(security): enforce team membership validation in admin UI#3399

Merged
crivetimihai merged 6 commits intomainfrom
bugfix/team-id-default
Mar 6, 2026
Merged

fix(security): enforce team membership validation in admin UI#3399
crivetimihai merged 6 commits intomainfrom
bugfix/team-id-default

Conversation

@madhav165
Copy link
Copy Markdown
Collaborator

@madhav165 madhav165 commented Mar 2, 2026

🐛 Bug-fix PR

Fixes https://github.ibm.com/contextforge-org/mcp-context-forge/issues/44


📌 Summary

Fixes a team scoping validation issue in the admin UI where an invalid team_id query parameter could result in broader visibility than intended. The endpoint now correctly enforces team membership checks and rejects unauthorized team contexts.

🐞 Root Cause

The admin_ui endpoint in mcpgateway/admin.py had a permissive fallback path during team validation that silently relaxed scoping constraints instead of enforcing them.

💡 Fix Description

  • Replaced the permissive fallback with strict 403 rejection when team membership validation fails.
  • The fix is scoped to the two validation branches inside the admin_ui function; the default no-team-id path remains unchanged.
  • Added three unit tests covering the invalid, unavailable, and null team_id cases.

🧪 Verification

Check Command Status
Lint suite make lint pass
Unit tests make test pass
Coverage ≥ 80 % make coverage pass

📐 MCP Compliance (if relevant)

  • No breaking change to MCP clients
  • Admin UI only; no protocol-level changes

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • No secrets/credentials committed
  • Deny-path regression tests included

@madhav165 madhav165 force-pushed the bugfix/team-id-default branch 3 times, most recently from a476370 to 2af20d3 Compare March 3, 2026 11:37
@crivetimihai crivetimihai changed the title [BUG]: Fix team scoping validation issue in admin UI fix(security): enforce team membership validation in admin UI Mar 5, 2026
@crivetimihai crivetimihai added bug Something isn't working security Improves security MUST P1: Non-negotiable, critical requirements without which the product is non-functional or unsafe labels Mar 5, 2026
@crivetimihai crivetimihai added this to the Release 1.0.0-RC2 milestone Mar 5, 2026
@crivetimihai
Copy link
Copy Markdown
Member

Thanks @madhav165 — important security fix. Replacing the permissive fallback with strict 403 rejection is the correct approach. Three unit tests covering the edge cases is good. LGTM.

MohanLaksh
MohanLaksh previously approved these changes Mar 6, 2026
Copy link
Copy Markdown
Collaborator

@MohanLaksh MohanLaksh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Team Scoping Validation Fix (PR #3399)

Executive Summary

Verdict: ✅ APPROVED

Architecture Compliance

Two-Layer Security Model:

Layer 1 (Token Scoping) → Layer 2 (RBAC) → Operation
     ↓                          ↓
normalize_token_teams()    require_permission()
     ↓                          ↓
  [PASS]                     [PASS]  → admin_ui() validates team_id

This fix operates at the application layer (after both security layers):

  • Token scoping already filtered what user CAN SEE
  • RBAC already checked if user CAN ACCESS admin dashboard
  • This fix ensures user can't REQUEST a team context they don't belong to

Correct placement in security stack:


🚨 Potential Issues & Mitigations

Issue 1: Breaking Change for Existing Clients

Risk: Low
Impact: Clients relying on fallback behavior will now get 403
Mitigation: This is the intended behavior - the fallback was a bug. Clients should not be requesting team contexts they don't belong to.

Issue 2: User Experience

Risk: Low
Impact: Users might see 403 instead of empty results
Mitigation: Error messages are clear ("Not a member of the requested team"). This is better UX than silently showing different data.

Issue 3: Service Availability

Risk: Low
Impact: If team service fails, users can't access admin UI with team filter
Mitigation: Users can still access without team_id parameter. This is correct fail-closed behavior.


🎓 Recommendations

For Merge

APPROVE - This PR should be merged as-is.

Rationale:

  1. Fixes the issue
  2. Implementation is correct and minimal
  3. Test coverage is comprehensive
  4. Aligns with project security principles
  5. No breaking changes to legitimate use cases

Test Running Results:

tests/unit/mcpgateway/test_admin_module.py::test_admin_ui_rejects_invalid_team_id PASSED
tests/unit/mcpgateway/test_admin_module.py::test_admin_ui_rejects_team_id_when_teams_unavailable PASSED
tests/unit/mcpgateway/test_admin_module.py::test_admin_ui_no_team_id_returns_public_items PASSED

🏆 Final Verdict

APPROVED ✅

Recommendation: Merge immediately

@MohanLaksh MohanLaksh added the release-fix Critical bugfix required for the release label Mar 6, 2026
@madhav165 madhav165 force-pushed the bugfix/team-id-default branch from 2af20d3 to a015922 Compare March 6, 2026 12:38
…k to public visibility

When a team_id query param does not match the user's teams, the admin UI
silently dropped the team filter and returned public entities. Now returns
403 Forbidden for both invalid team_id and failed team lookup cases.
Adds regression tests for invalid, unavailable, and null team_id paths.

Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>
Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>
@madhav165 madhav165 force-pushed the bugfix/team-id-default branch from a015922 to dde19cb Compare March 6, 2026 14:15
Platform admins should be able to view any team's resources in the admin
UI, consistent with the codebase-wide admin bypass pattern (token scoping,
RBAC middleware, permission service all skip team checks for admins).

- Add is_admin check before team membership validation so platform admins
  can select any team_id without being a member
- Rename test_admin_ui_team_loading_failure_ignores_team_id to
  test_admin_ui_team_loading_failure_rejects_team_id (matches new 403 behavior)
- Add test_admin_ui_admin_bypasses_team_membership_check for coverage of
  the admin bypass path

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
The admin bypass must check both is_admin AND token_teams is None,
consistent with the codebase convention (admin.py:13253, base_service.py,
token scoping middleware). A team-scoped admin token (is_admin=True,
token_teams=["team-1"]) must still pass membership validation.

- Check _token_teams is None alongside _is_admin for the bypass
- Update comment to document the token_teams requirement
- Add test for team-scoped admin rejection (token_teams=["team-1"])
- Update admin bypass test to set token_teams=None explicitly

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

Review Summary

Rebased onto main (clean), reviewed, and applied fixes.

Original PR (correct)

  • Replaces the permissive fallback (silently dropping team_id to public visibility) with strict 403 rejection when a non-admin user supplies a team_id they don't belong to.
  • Adds deny-path regression tests for invalid team_id, unavailable teams, and null team_id.

Review fixes applied

1. Admin bypass with token_teams check
The original PR blocked platform admins from viewing other teams' resources. Added an admin bypass consistent with the codebase convention: requires both is_admin=True AND token_teams is None (unrestricted token). Team-scoped admin tokens (token_teams=["team-1"]) are still subject to membership validation.

2. Test name correction
Renamed test_admin_ui_team_loading_failure_ignores_team_idtest_admin_ui_team_loading_failure_rejects_team_id to match the new 403 behavior.

3. Additional tests

  • test_admin_ui_admin_bypasses_team_membership_check — unrestricted admin can select any team
  • test_admin_ui_team_scoped_admin_rejected_for_other_team — scoped admin gets 403 for non-member team

4. Docstring Raises section
Added HTTPException to the admin_ui docstring to satisfy darglint (DAR401).

Known pre-existing limitation (not in scope)

admin_ui does not pass token_teams through to service methods, so downstream _apply_access_control falls back to DB-derived team membership. This means even unrestricted admins see empty results for teams they're not members of. This predates this PR and should be addressed separately.

Verification

  • All tests pass independently (test_admin_module.py, test_admin.py)
  • flake8 DAR401 clean
  • 100% differential coverage on new/changed lines

Copy link
Copy Markdown
Member

@crivetimihai crivetimihai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Security fix is correct — 403 rejection for unauthorized team_id with proper admin bypass (is_admin + token_teams=None). All branches tested.

@crivetimihai crivetimihai merged commit 36ea58e into main Mar 6, 2026
39 checks passed
@crivetimihai crivetimihai deleted the bugfix/team-id-default branch March 6, 2026 18:14
@crivetimihai crivetimihai self-assigned this Mar 9, 2026
MohanLaksh pushed a commit that referenced this pull request Mar 12, 2026
* fix(security): reject invalid team_id with 403 instead of falling back to public visibility

When a team_id query param does not match the user's teams, the admin UI
silently dropped the team filter and returned public entities. Now returns
403 Forbidden for both invalid team_id and failed team lookup cases.
Adds regression tests for invalid, unavailable, and null team_id paths.

Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>

* pytest and pylint fixes

Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>

* fix(security): add admin bypass to team_id validation and fix test name

Platform admins should be able to view any team's resources in the admin
UI, consistent with the codebase-wide admin bypass pattern (token scoping,
RBAC middleware, permission service all skip team checks for admins).

- Add is_admin check before team membership validation so platform admins
  can select any team_id without being a member
- Rename test_admin_ui_team_loading_failure_ignores_team_id to
  test_admin_ui_team_loading_failure_rejects_team_id (matches new 403 behavior)
- Add test_admin_ui_admin_bypasses_team_membership_check for coverage of
  the admin bypass path

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(security): update comment to reflect admin bypass behavior

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(security): add HTTPException to admin_ui docstring Raises section

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(security): tighten admin bypass to require unrestricted token_teams

The admin bypass must check both is_admin AND token_teams is None,
consistent with the codebase convention (admin.py:13253, base_service.py,
token scoping middleware). A team-scoped admin token (is_admin=True,
token_teams=["team-1"]) must still pass membership validation.

- Check _token_teams is None alongside _is_admin for the bypass
- Update comment to document the token_teams requirement
- Add test for team-scoped admin rejection (token_teams=["team-1"])
- Update admin bypass test to set token_teams=None explicitly

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Yosiefeyob pushed a commit that referenced this pull request Mar 13, 2026
* fix(security): reject invalid team_id with 403 instead of falling back to public visibility

When a team_id query param does not match the user's teams, the admin UI
silently dropped the team filter and returned public entities. Now returns
403 Forbidden for both invalid team_id and failed team lookup cases.
Adds regression tests for invalid, unavailable, and null team_id paths.

Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>

* pytest and pylint fixes

Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>

* fix(security): add admin bypass to team_id validation and fix test name

Platform admins should be able to view any team's resources in the admin
UI, consistent with the codebase-wide admin bypass pattern (token scoping,
RBAC middleware, permission service all skip team checks for admins).

- Add is_admin check before team membership validation so platform admins
  can select any team_id without being a member
- Rename test_admin_ui_team_loading_failure_ignores_team_id to
  test_admin_ui_team_loading_failure_rejects_team_id (matches new 403 behavior)
- Add test_admin_ui_admin_bypasses_team_membership_check for coverage of
  the admin bypass path

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(security): update comment to reflect admin bypass behavior

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(security): add HTTPException to admin_ui docstring Raises section

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(security): tighten admin bypass to require unrestricted token_teams

The admin bypass must check both is_admin AND token_teams is None,
consistent with the codebase convention (admin.py:13253, base_service.py,
token scoping middleware). A team-scoped admin token (is_admin=True,
token_teams=["team-1"]) must still pass membership validation.

- Check _token_teams is None alongside _is_admin for the bypass
- Update comment to document the token_teams requirement
- Add test for team-scoped admin rejection (token_teams=["team-1"])
- Update admin bypass test to set token_teams=None explicitly

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Madhav Kandukuri <madhav165@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: Yosief Eyob <yosiefogbazion@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working MUST P1: Non-negotiable, critical requirements without which the product is non-functional or unsafe release-fix Critical bugfix required for the release security Improves security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants