Skip to content

Add org-level access groups for MCP connections - #1597

Open
TheLiberal wants to merge 2 commits into
UsefulSoftwareCo:mainfrom
TheLiberal:access-groups/1-enforcement
Open

Add org-level access groups for MCP connections#1597
TheLiberal wants to merge 2 commits into
UsefulSoftwareCo:mainfrom
TheLiberal:access-groups/1-enforcement

Conversation

@TheLiberal

Copy link
Copy Markdown

What

Department-level access control for org connections: a connection can be restricted to a named access group, and for anyone outside the group it does not exist — on any read or invoke surface, with no existence oracle.

  • Two new tenant-scoped tables (access_group, access_group_member) built on the same tenantExecutorTable factory as subject, plus a nullable connection.access_group column (nullable is required for SQLite boot-ensure hosts). owner-policy.ts and the Owner union are untouched — groups are an additive axis, not a third owner scope.
  • Enforcement lives at the core executor funnels: toolsList (unconditionally, outside includeBlocked — the HTTP tools list defaults includeBlocked=true, so a policy-block model would leak on the wire), connectionsList/get/update/remove/refresh/checkHealth, toolSchema, and execute. In execute the gate runs before policy resolution and not-found suggestions: a blocked verdict or a suggestion list naming a hidden connection's tools would each be an oracle. Hidden ≡ nonexistent: same errors, empty suggestions.
  • Membership is read live per call — a roster edit applies on the next call of an already-open MCP session.
  • The platform view and subject-less org bindings (org API keys) stay unfiltered by design; regression-tested. Admin runtime sessions are filtered like everyone's — no is-admin plumbing in the executor; management rides executor.accessGroups behind each host's existing admin gate (cloud: WorkOS admin-role session; self-host: requireInstanceAdmin), never the any-member ExecutorApi.
  • Referential integrity is service-layer (this schema has no FKs): restricting requires the group to exist, deleting a group is refused while a connection references it, membership add is idempotent. Org deletion purges the new tables (the cascade-coverage test drove this).
  • Toolkit listConnections drops literal <integration>.org.<connection> patterns that don't resolve for the caller — the pattern text is otherwise an oracle.

Tests

  • packages/core/sdk/src/access-groups.test.ts — CRUD + integrity rules.
  • packages/core/sdk/src/access-group-enforcement.test.ts — the red-team invariants: no read/invoke path returns a restricted row to a non-member (incl. includeBlocked=true), non-member ≡ nonexistent error parity, an org policy approve cannot re-expose a restricted tool, live mid-session roster edits, unfiltered platform/subject-less views, unrestricted rows unaffected.
  • Host gate tests on both hosts, including a node test over the real booted self-host app (401/403 + admin round trip).
  • Migrations: cloud drizzle 0016 (additive); local/self-host/cloudflare boot-ensure from coreTables.

Part 1 of 3 (followed by per-toolkit grants and the console UI).

Tenant-scoped access_group/access_group_member tables plus a nullable
connection.access_group column. A restricted org connection is invisible
and uninvokable for non-members on every read and invoke surface, with
no existence oracle; membership is read live per call. Management rides
executor.accessGroups behind host admin gates (cloud /org/access-groups*,
self-host /api/admin/access-groups*), never the any-member ExecutorApi.
Platform view and subject-less org bindings stay unfiltered by design.
The cascade tripwire caught the gap: the hand-written purge list must name
every tenant table, and access_group/access_group_member were missing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant