feat(rbac): OpenFGA authz, team-invoke + org-wide sharing for custom MCP tools#1711
Merged
Conversation
Member
Author
Follow-up commits (same MCP-tool authz theme)
All new/affected UI tests pass (79 in the rbac + rag-route suites). |
Contributor
🧪 CAIPE UI Test Results✅ All tests passed 🟠 Overall Coverage: 55%📊 Detailed Coverage
✅ Test Suites
📈 Coverage Thresholds
|
5 tasks
caipe-ci-bot
previously approved these changes
Jun 4, 2026
…rse admin
The RAG server gated POST/PUT/DELETE /v1/mcp/custom-tools on
require_role(Role.ADMIN), but human users are always assigned Role.READONLY
(rbac.py never elevates a human), so no human could ever create, update, or
delete a custom MCP tool. The create flow added in the unified shareable-
resource RBAC work always returned 403 Forbidden ("required admin, has
readonly"), regardless of the caller OpenFGA ownership or org-admin status.
This migrates the three custom-tool write endpoints to OpenFGA-based
authorization, matching the documented model ("tool authorization comes from
OpenFGA relationships", rag/README.md) and the BFF/UI ownership semantics:
- create: org admin OR member of the requested owner team
(team:<owner_team_slug>#can_use)
- update/delete: mcp_tool:<tool_id>#can_manage (owner, owner-team admin, or
org admin)
Both helpers fail CLOSED: 403 when unauthorized, 503 when the OpenFGA PDP is
unavailable or not configured. Coarse-ADMIN service principals (admin
client-credentials tokens, CAIPE_UNSAFE_RBAC_BYPASS) are still permitted so
existing automation is not regressed.
Adds unit tests covering org-admin, owner-team-member, can_manage, denial, and
fail-closed (PDP error / not configured / no subject) paths.
Assisted-by: Cursor claude-opus-4.8
Signed-off-by: Sri Aradhyula <sraradhy@cisco.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Two related gaps in the unified shareable-resource RBAC for custom MCP tools, plus a UI polish: 1. can_call on team shares. Shared/owner team members were granted `reader` + `user` but not `caller`, so `can_call` (the invoke gate) denied them even though the tool showed up in their list. Member grants now also write `caller`; the `mcp_tool_grants_backfill_v1` migration backfills it for existing rows. Without this, sharing a KB-search tool with a team left its members unable to actually search. 2. Org-wide sharing. New `shared_with_org` flag on `MCPToolConfig` (Python + TS). When set, the reconciler grants `organization#member` reader/user/caller on the tool; turning it off revokes them. The OpenFGA model (`model.fga` + chart `authorization-model.json`) now lists `organization#member` as an allowed subject for `mcp_tool` reader/user/caller. Threaded through the BFF route (read previous state from config for the revoke diff; persist the flag on POST/PUT) and exposed as a "Share with the whole organization" toggle in the MCP tool editor. This makes the org-wide `caipe_kb` grant durable instead of a hand-applied tuple. Also: the MCP tools view now renders canonical datasource names (with the id in the tooltip) in both the create dialog and the saved-tool cards, instead of raw `src_...` ids. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Sri Aradhyula <sraradhy@cisco.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Members of the bootstrapped `super-admins` team now receive org-admin authority by writing the `team:super-admins#admin -> admin -> organization:<key>` tuple during team bootstrap. This lets super-admins manage and share any resource (e.g. create/transfer custom MCP tools) without per-resource grants. Idempotent: existing teams are topped up on the next bootstrap. Covered by new unit tests. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Sri Aradhyula <sraradhy@cisco.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The e2e suite targets a live stack on localhost:9446 with specific env (ENABLE_GRAPH_RAG, etc.), so it fails by default in unit-test runs. Gate the module with a skipif so it only runs when RAG_E2E=1, keeping the default `pytest` run green. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Sri Aradhyula <sraradhy@cisco.com> Co-authored-by: Cursor <cursoragent@cursor.com>
333c990 to
4310a9c
Compare
|
✅ No proprietary content detected. This PR is clear for review! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Custom MCP tool creation from the UI always failed with 403 Forbidden, even for org admins and owner-team members. The RAG server gates
POST/PUT/DELETE /v1/mcp/custom-toolsonrequire_role(Role.ADMIN), butrbac.pyassigns every human userRole.READONLYwith no elevation path — so no human can ever create/update/delete a custom MCP tool. The unified shareable-resource RBAC work added themcp_toolOpenFGA type, ownership/sharing UI, and BFF tuple-writing, but left these server endpoints on the legacy coarse-admin gate.Observed failure:
This migrates the three custom-tool write endpoints to OpenFGA-based authorization, matching the documented model (
rag/README.md: "tool authorization comes from OpenFGA relationships") and the BFF/UI ownership semantics already shipped in this feature branch.Changes
server/rbac.py— new fail-closed helpers:authorize_mcp_tool_create(user, owner_team_slug)— allow if org admin or member of the requested owner team (team:<slug>#can_use).authorize_mcp_tool_manage(user, tool_id)— allow if org admin ormcp_tool:<tool_id>#can_manage(owner, owner-team admin, org admin).ADMINservice principals (admin client-credentials tokens,CAIPE_UNSAFE_RBAC_BYPASS) are still permitted first, so existing automation is not regressed.server/restapi.py—POST/PUT/DELETE /v1/mcp/custom-toolsnow depend onrequire_authenticated_userand call the new authz helpers (replacingrequire_role(Role.ADMIN)).tests/test_mcp_tool_authz.py— 13 unit tests: coarse-admin bypass, org-admin, owner-team-member,can_manage, denial (403), and fail-closed paths (PDP error / not configured / no subject).Why this is the right layer
The RAG server is a real, directly-reachable enforcement point, so authorization must live here (not only in the BFF). On create the tool has no tuples yet, so we mirror the BFF first-set rule (org-admin or owner-team membership); on update/delete the per-resource
mcp_tool#can_managerelation exists and is authoritative.Test plan
uv run ruff checkon changed files — cleanuv run pytest tests/test_mcp_tool_authz.py tests/test_openfga_team_rebac.py— 24 passedrag-serverimage, recreated container, verified startup +/health200