feat(mcp): org-scope MCP enrollment to the subdomain#443
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report❌ Patch coverage is 📢 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>
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.
Implements
docs/design/mcp-enrollment-org-scoping.md(now Implemented), recorded as DECISIONS D26.Problem
On a corp subdomain (
<slug>.api.overslash.com),/oauth/authorizederived the enrollment org from the session (pending.org_id = session_claims.org), consulting the subdomainctxonly for the cold-login IdP bounce. So an employee holding a personal-org session who hitacme.api.overslash.com/oauth/authorizewhile 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 isorg_id, unconditionally. Root apex stays the multi-org hub — all changes are additive on the subdomain path.authorize(): derive the enrollment org fromctx; 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 andpending.org_id.register(): stampoauth_mcp_clients.org_idfromctx(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.095: addoauth_mcp_clients.org_id; migrate the binding UNIQUE from(user_identity_id, client_id)→(user_identity_id, client_id, org_id).mcp_client_agent_binding::get_forgains anorg_idparam;upsertON CONFLICT includesorg_id;oauth_mcp_clientcarriesorg_id+ a newlist_for_org.NULLclients 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_jwtis already a no-op atRoot. Back-compat: pre-migrationNULL-org_idclients keep working as "any subdomain."Tests
New
crates/overslash-api/tests/oauth_mcp_org_scoping.rscovers 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; existingmcp_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 warningsclean;make check-sqlxin sync.Dashboard
No component change — Org Settings → MCP Clients renders whatever
/v1/oauth/mcp-clientsreturns, now backend-scoped to the admin's org. There is no visual diff to screenshot.Notes
mcp-enrollment-org-scoping.md), which is not yet ondevand which the implementation edits (status flip). BaseSCHEMA.sqlis identical todev's, so the schema patch merges cleanly.🤖 Generated with Claude Code