Skip to content

feat(rbac): OpenFGA authz, team-invoke + org-wide sharing for custom MCP tools#1711

Merged
sriaradhyula merged 5 commits into
mainfrom
fix/rag-mcp-tool-openfga-authz
Jun 4, 2026
Merged

feat(rbac): OpenFGA authz, team-invoke + org-wide sharing for custom MCP tools#1711
sriaradhyula merged 5 commits into
mainfrom
fix/rag-mcp-tool-openfga-authz

Conversation

@sriaradhyula
Copy link
Copy Markdown
Member

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-tools on require_role(Role.ADMIN), but rbac.py assigns every human user Role.READONLY with no elevation path — so no human can ever create/update/delete a custom MCP tool. The unified shareable-resource RBAC work added the mcp_tool OpenFGA type, ownership/sharing UI, and BFF tuple-writing, but left these server endpoints on the legacy coarse-admin gate.

Observed failure:

[rag.server.rbac] WARNING - Access denied for <user>: required admin, has readonly
POST /v1/mcp/custom-tools 403 Forbidden

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 or mcp_tool:<tool_id>#can_manage (owner, owner-team admin, org admin).
    • Both return 403 when unauthorized and 503 when the OpenFGA PDP is unavailable / not configured / the caller has no stable subject.
    • Coarse-ADMIN service principals (admin client-credentials tokens, CAIPE_UNSAFE_RBAC_BYPASS) are still permitted first, so existing automation is not regressed.
  • server/restapi.pyPOST/PUT/DELETE /v1/mcp/custom-tools now depend on require_authenticated_user and call the new authz helpers (replacing require_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_manage relation exists and is authoritative.

Test plan

  • uv run ruff check on changed files — clean
  • uv run pytest tests/test_mcp_tool_authz.py tests/test_openfga_team_rebac.py — 24 passed
  • Rebuilt rag-server image, recreated container, verified startup + /health 200
  • Create a custom MCP tool from the UI as an owner-team member → 201 and the tool appears in the list
  • Non-member without org admin → 403
  • Update/delete by a non-owner non-admin → 403

Pre-existing (not introduced here): several server tests (test_doc_acl.py, test_role_mapping.py, test_keycloak_oidc_auth.py) fail on this branch because UserContext now requires a groups field they don't pass; test_e2e.py needs a live server.

@sriaradhyula sriaradhyula changed the title fix(rag): authorize custom MCP tool writes via OpenFGA instead of coarse admin feat(rbac): OpenFGA authz, team-invoke + org-wide sharing for custom MCP tools Jun 4, 2026
@sriaradhyula
Copy link
Copy Markdown
Member Author

Follow-up commits (same MCP-tool authz theme)

  • feat(rbac): org-wide and team-invoke sharing for custom MCP tools (2d33ae3f5)
    • Fixes the latent bug where shared/owner team members got reader + user but not caller, so can_call denied them and KB-search tools couldn't actually be invoked. Member grants now write caller; the mcp_tool_grants_backfill_v1 migration backfills existing rows.
    • Adds first-class org-wide sharing: new shared_with_org flag on MCPToolConfig (Python + TS), reconciler emits/revokes organization#member reader/user/caller, OpenFGA model + chart authorization-model.json allow organization#member as a subject for mcp_tool, BFF route threads + persists the flag, and a "Share with the whole organization" toggle in the tool editor. Makes the previously hand-applied caipe_kb org grant durable.
    • UI polish: MCP tools view shows canonical datasource names (id in tooltip) instead of raw src_... ids.
  • feat(rbac): grant org-admin to super-admins team members (2b149abf4) — bootstraps team:super-admins#admin -> admin -> organization:<key>.
  • test(rag): make RAG server e2e tests opt-in via RAG_E2E (333c990cd) — keeps the default pytest run green.

All new/affected UI tests pass (79 in the rbac + rag-route suites). caipe_kb itself is a runtime tool (no config file), so its shared_with_org flag becomes durable once it's re-saved with the toggle on after the next caipe-ui-prod rebuild.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

🧪 CAIPE UI Test Results

All tests passed

🟠 Overall Coverage: 55%

Coverage
lines
statements
functions
branches

📊 Detailed Coverage

Metric Covered Total Percentage
Lines 25710 42735 60.16%
Statements 27397 46986 58.30%
Functions 4653 8645 53.82%
Branches 16738 34090 49.09%

✅ Test Suites

  • ✅ auth-guard.test.tsx - Route protection & authorization
  • ✅ token-expiry-guard.test.tsx - Token expiry handling
  • ✅ a2a-sdk-client.test.ts - A2A streaming SDK
  • ✅ auth-utils.test.ts - Authentication utilities (100% coverage)
  • ✅ auth-config.test.ts - OIDC configuration
📈 Coverage Thresholds
Threshold Target Current Status
Minimum 40% 55% ✅ Pass
Good 60% 55% ⚠️ Below target
Excellent 80% 55% ⚠️ Below target
⚠️ Areas Needing Tests

High Priority:

  • hooks/use-a2a-streaming.ts - Core streaming functionality
  • store/chat-store.ts - Chat state management
  • store/agent-skills-store.ts - Agent skills
  • lib/api-client.ts - API communication
  • lib/storage-mode.ts - MongoDB/localStorage switching

Medium Priority:

  • components/chat/ChatPanel.tsx - Main chat interface
  • components/agent-builder/* - Agent builder UI
  • lib/mongodb.ts - MongoDB integration

💡 Run locally: make caipe-ui-tests
📦 Full report: Check workflow artifacts

Base automatically changed from feat/unified-shareable-resource-rbac to main June 4, 2026 05:15
caipe-ci-bot
caipe-ci-bot previously approved these changes Jun 4, 2026
sriaradhyula and others added 4 commits June 4, 2026 00:23
…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>
@caipe-security
Copy link
Copy Markdown

caipe-security Bot commented Jun 4, 2026

✅ No proprietary content detected. This PR is clear for review!

@github-actions github-actions Bot added the dev Normal PR to main; uses dev prerelease versioning label Jun 4, 2026
@sriaradhyula sriaradhyula merged commit e37afb4 into main Jun 4, 2026
49 of 50 checks passed
@sriaradhyula sriaradhyula deleted the fix/rag-mcp-tool-openfga-authz branch June 4, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev Normal PR to main; uses dev prerelease versioning

Development

Successfully merging this pull request may close these issues.

2 participants