Skip to content

feat(mcp): org-scope MCP enrollment to the subdomain#443

Merged
angel-manuel merged 2 commits into
devfrom
mcp-enrollment-org-scoping
Jul 9, 2026
Merged

feat(mcp): org-scope MCP enrollment to the subdomain#443
angel-manuel merged 2 commits into
devfrom
mcp-enrollment-org-scoping

Conversation

@angel-manuel

Copy link
Copy Markdown
Contributor

Implements docs/design/mcp-enrollment-org-scoping.md (now Implemented), recorded as DECISIONS D26.

Problem

On a corp subdomain (<slug>.api.overslash.com), /oauth/authorize derived the enrollment org from the session (pending.org_id = session_claims.org), consulting the subdomain ctx only for the cold-login IdP bounce. So an employee holding a personal-org session who hit acme.api.overslash.com/oauth/authorize while authenticated enrolled the agent into their personal org — the subdomain governed discovery but not where the agent landed. The corporate guarantee ("point our MDM-managed Claude at our subdomain and trust every enrolled agent is ours") did not hold.

Change

The subdomain is now the enrollment boundary. On RequestOrgContext::Org { org_id } the enrolled agent's org is org_id, unconditionally. Root apex stays the multi-org hub — all changes are additive on the subdomain path.

  • authorize(): derive the enrollment org from ctx; re-auth a warm session whose org ≠ the subdomain org through the org IdP (idp_bounce, next= preserved) instead of enrolling into the stale session org; org-scope the fast-path binding lookup and pending.org_id.
  • register(): stamp oauth_mcp_clients.org_id from ctx (subdomain → that org; root → NULL = multi-org). authorize() rejects a client stamped for a different org (cross-subdomain replay protection); NULL/root clients are accepted.
  • Migration 095: add oauth_mcp_clients.org_id; migrate the binding UNIQUE from (user_identity_id, client_id)(user_identity_id, client_id, org_id).
  • Repos: mcp_client_agent_binding::get_for gains an org_id param; upsert ON CONFLICT includes org_id; oauth_mcp_client carries org_id + a new list_for_org.
  • Admin surface: Org Settings → MCP Clients list + revoke are scoped to the admin's own org (root/NULL clients belong to no org and are invisible there).

Root apex unchanged

No subdomain to honor → enrollment follows the session org (which may be a corp org the user belongs to), and no re-auth-on-mismatch fires. check_subdomain_matches_jwt is already a no-op at Root. Back-compat: pre-migration NULL-org_id clients keep working as "any subdomain."

Tests

New crates/overslash-api/tests/oauth_mcp_org_scoping.rs covers the design's 7-item plan (stale-session bounce, matching-session enrollment, cross-org binding isolation, cross-subdomain client rejection, root multi-org, NULL-client back-compat). All 6 tests pass; existing mcp_oauth (30), multi_org (28), mcp_elicitation (16), mcp_approve_split (6), subdomain_oauth_as (3) suites stay green. cargo clippy --workspace --all-targets -D warnings clean; make check-sqlx in sync.

Dashboard

No component change — Org Settings → MCP Clients renders whatever /v1/oauth/mcp-clients returns, now backend-scoped to the admin's org. There is no visual diff to screenshot.

Notes

  • This PR carries the design doc itself (docs(design): layered service templates + MCP enrollment org-scoping #439's mcp-enrollment-org-scoping.md), which is not yet on dev and which the implementation edits (status flip). Base SCHEMA.sql is identical to dev's, so the schema patch merges cleanly.
  • Client admission (which software may enroll) is out of scope; org-scoping is designed to compose with it later.

🤖 Generated with Claude Code

On a corp subdomain (`<slug>.api.overslash.com`), `/oauth/authorize` now
enrolls the agent into that org unconditionally instead of into the
session's org. Implements docs/design/mcp-enrollment-org-scoping.md (D26).

- authorize(): derive enrollment org from the subdomain `ctx`; re-auth a
  mismatched warm session through the org IdP (`idp_bounce`, `next=`
  preserved) rather than enrolling into the stale session org; org-scope
  the fast-path binding lookup and `pending.org_id`.
- register(): stamp `oauth_mcp_clients.org_id` from `ctx` (subdomain → that
  org, root → NULL/multi-org). authorize() rejects a client stamped for a
  different org (cross-subdomain replay protection); NULL is accepted.
- Migration 095: add `oauth_mcp_clients.org_id` and migrate the binding
  UNIQUE from `(user_identity_id, client_id)` to `(…, org_id)`.
- Repos: `mcp_client_agent_binding::get_for` gains an `org_id` param;
  `upsert` ON CONFLICT includes `org_id`; `oauth_mcp_client` carries
  `org_id` + `list_for_org`.
- Admin Org Settings → MCP Clients list/revoke scoped to the admin's org.
- Root apex stays the multi-org hub (enrollment follows the session org).

Tests: crates/overslash-api/tests/oauth_mcp_org_scoping.rs (7-item plan).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
overslash Ready Ready Preview, Comment Jul 8, 2026 10:13pm

Request Review

Comment thread crates/overslash-api/src/routes/oauth.rs
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 18 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/overslash-db/src/repos/oauth_mcp_client.rs 10.00% 9 Missing ⚠️
...ates/overslash-api/src/routes/oauth_mcp_clients.rs 0.00% 6 Missing ⚠️
crates/overslash-api/src/routes/oauth.rs 94.54% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

consent_switch_org clones a parked authorize request into another org the
user belongs to while keeping the original client_id, bypassing the
authorize-time client-org gate. Add the authoritative check at
consent_finish — the single agent-binding-creation site — so a client
stamped for one org can't bind an agent in a different org via the switch
path. NULL (root/multi-org) clients are unaffected.

Regression: stamped_client_cannot_bind_in_switched_org.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@angel-manuel angel-manuel merged commit 932f64c into dev Jul 9, 2026
15 checks passed
@angel-manuel angel-manuel deleted the mcp-enrollment-org-scoping branch July 9, 2026 05:40
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