mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: harden org-scoped MCP config chat gating, updates, and visibility (#28065)
Hardens the org-scoped MCP server config surface from #27942 with fixes and regression pins that are independent of the core cutover: - Keep chats sendable after a selected MCP server is disabled (persisted selections are exempt from message-time rejection). - Keep cached MCP selections usable after a background refetch error (gate the composer on missing data, not `isSuccess`). - Merge PATCH updates onto the current row so unset fields are not clobbered. - Give auditors the full management view of MCP configs their audit logs reference (site and org auditor roles). - Regression pins: frozen OAuth2 callback path, cross-org concealment for item routes, OAuth callback token binding, and disconnect responses indistinguishable from nonexistent config IDs. - Storybook: semantic textbox queries in MCP loading stories; docs note that the MCP settings page needs deployment access. ## Stack context Part of the MCP org-separation stack (CODAGT-711 org scope -> apidocs -> hardening -> CODAGT-717 audit -> CODAGT-712 ACLs -> CODAGT-806 token RBAC). Split out of #27942 to keep the core cutover reviewable; each change here builds on the org-scoped routes and chat gating introduced below it. > Mux (AI agent) authored this PR on Mike's behalf. <!-- mux-attribution: model=claude-fable-5 thinking=high -->
This commit is contained in:
+25
-6
@@ -2146,16 +2146,30 @@ func TestMCPServerOAuth2PKCE(t *testing.T) {
|
||||
require.NotEmpty(t, query.Get("code_challenge"),
|
||||
"connect redirect must include a code_challenge")
|
||||
|
||||
// A verifier cookie must be set.
|
||||
var verifierCookie *http.Cookie
|
||||
// The callback path is frozen because it is registered as a
|
||||
// redirect URI with external authorization servers.
|
||||
frozenCallbackPath := "/api/experimental/mcp/servers/" + created.ID.String() + "/oauth2/callback"
|
||||
redirectURI, err := url.Parse(query.Get("redirect_uri"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, frozenCallbackPath, redirectURI.Path,
|
||||
"outbound redirect_uri must use the frozen callback path")
|
||||
|
||||
var stateCookie, verifierCookie *http.Cookie
|
||||
for _, c := range res.Cookies() {
|
||||
if c.Name == "mcp_oauth2_verifier_"+created.ID.String() {
|
||||
switch c.Name {
|
||||
case "mcp_oauth2_state_" + created.ID.String():
|
||||
stateCookie = c
|
||||
case "mcp_oauth2_verifier_" + created.ID.String():
|
||||
verifierCookie = c
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, stateCookie, "response must set a state cookie")
|
||||
require.Equal(t, frozenCallbackPath, stateCookie.Path,
|
||||
"state cookie must be scoped to the frozen callback path")
|
||||
require.NotNil(t, verifierCookie, "response must set a PKCE verifier cookie")
|
||||
require.NotEmpty(t, verifierCookie.Value)
|
||||
require.Equal(t, frozenCallbackPath, verifierCookie.Path,
|
||||
"verifier cookie must be scoped to the frozen callback path")
|
||||
|
||||
// Verify the code_challenge matches SHA256(verifier).
|
||||
h := sha256.Sum256([]byte(verifierCookie.Value))
|
||||
@@ -2260,12 +2274,17 @@ func TestMCPServerOAuth2PKCE(t *testing.T) {
|
||||
"token exchange must send the PKCE code_verifier")
|
||||
|
||||
// Verify the verifier cookie is cleared in the response.
|
||||
var clearedVerifier *http.Cookie
|
||||
for _, c := range res.Cookies() {
|
||||
if c.Name == "mcp_oauth2_verifier_"+created.ID.String() {
|
||||
require.Equal(t, -1, c.MaxAge,
|
||||
"verifier cookie must be cleared after callback")
|
||||
clearedVerifier = c
|
||||
}
|
||||
}
|
||||
require.NotNil(t, clearedVerifier, "callback must clear the verifier cookie")
|
||||
require.Equal(t, -1, clearedVerifier.MaxAge,
|
||||
"verifier cookie must be cleared after callback")
|
||||
require.Equal(t, callbackURL.Path, clearedVerifier.Path,
|
||||
"cleared verifier cookie must be scoped to the frozen callback path")
|
||||
})
|
||||
|
||||
t.Run("CallbackWithoutVerifierStillWorks", func(t *testing.T) {
|
||||
|
||||
@@ -1159,8 +1159,7 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {
|
||||
ResourceOrganization.Type: {policy.ActionRead},
|
||||
// Can read available roles.
|
||||
ResourceAssignOrgRole.Type: {policy.ActionRead},
|
||||
// TODO(mafredri): Remove once CODAGT-712 replaces this grant with
|
||||
// per-config ACL evaluation.
|
||||
// TODO(mafredri): remove once CODAGT-712 adds per-config ACL evaluation.
|
||||
ResourceMCPServerConfig.Type: {policy.ActionRead},
|
||||
}
|
||||
|
||||
@@ -1239,8 +1238,7 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions {
|
||||
ResourceOrganization.Type: {policy.ActionRead},
|
||||
// Can read available roles.
|
||||
ResourceAssignOrgRole.Type: {policy.ActionRead},
|
||||
// TODO(mafredri): Remove once CODAGT-712 replaces this grant with
|
||||
// per-config ACL evaluation.
|
||||
// TODO(mafredri): remove once CODAGT-712 adds per-config ACL evaluation.
|
||||
ResourceMCPServerConfig.Type: {policy.ActionRead},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user