Files
coder/coderd/rbac/object_gen.go
T
Michael SuchaczandMathias Fredriksson 443e3b9b80 feat!: org-scope MCP server configs with RBAC (#27942)
Moves `mcp_server_configs` from deployment scope to organization scope
so each organization fully controls the MCP servers its members can use
with Coder Agents.

## Summary

- Migration: adds `organization_id` (NOT NULL, FK) and keeps existing
rows as the default organization's originals with credentials intact.
Other organizations start with no MCP servers and configure their own;
nothing is copied across organizations. Chats outside the default
organization keep any now-cross-organization `mcp_server_ids` entries;
the runtime already ignores IDs that do not resolve in the chat's
organization, so no data rewrite is needed. Slug uniqueness becomes
`(organization_id, slug)`.
- RBAC: new org-scoped `ResourceMCPServerConfig` with regosql converter
and `GetAuthorizedMCPServerConfigs`; org admins get in-org CRUD, org
members get read (replaced by ACL evaluation in the follow-up ACL PR in
this stack).
- API: all config routes nest under the organization, matching
templates: `POST|GET
/api/experimental/organizations/{organization}/mcp-servers` and
`GET|PATCH|DELETE .../mcp-servers/{mcpserverconfig}` (plus
`oauth2/connect`), resolved by a read-only param middleware that
conceals read-denied and cross-organization access as 404. Two routes
stay on the frozen `/api/experimental/mcp/servers/{mcpServer}` block:
the OAuth2 callback (the redirect URI baked into existing AS-side client
registrations) and `oauth2/disconnect`, which must remain reachable by
users removed from the organization so they can still revoke their token
grant.
- Chat runtime: selection validation and generation resolve configs
strictly by IDs, enabled state, and the chat's organization in SQL;
requested duplicates are normalized; invalid or cross-org IDs are
rejected with the precise ID list. IDs already persisted on a chat are
exempt from message-time rejection so disabling a selected server never
blocks sends; generation skips servers that are no longer usable.
- Frontend: API layer and admin settings pages target the new endpoints.
The admin page manages the default organization's servers; the org
picker is tracked separately (CODAGT-714).

- Security hardening from review: OAuth user grants are additionally
bound to `oauth2_revocation_url` (changing it invalidates grants, and a
racing OAuth callback gets 409 instead of recreating a grant).
Stack-wide SSRF protection for MCP config-directed traffic was split
into its own PR at the top of this stack (#28242) to keep this diff
reviewable; this PR keeps main's existing discovery IP-range guard.
OAuth2 auto-discovery now completes before the config row is inserted: a
failed discovery persists nothing, and there is no provisional row that
concurrent updates could race against.

Two follow-up PRs in this stack were split out to keep this diff
reviewable: #28064 completes the swagger annotations for the moved
routes (main already ships these experimental MCP handlers unannotated),
and #28065 carries hardening fixes and regression pins on top of the
cutover.

- Force On enforcement (landed on main mid-review) is org-scoped: the
forced set is read per chat organization
(`GetForcedMCPServerConfigsByOrganization`), so another organization's
`force_on` server never attaches to a chat.

## Breaking changes (experimental API)

The MCP server config endpoints move from the deployment-scoped
`/api/experimental/mcp/servers` block to organization-nested paths:
`POST|GET /api/experimental/organizations/{organization}/mcp-servers`
and `GET|PATCH|DELETE
/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}`
(plus `oauth2/connect`). The old paths are removed, so API consumers
must supply an organization. Two routes intentionally stay on the frozen
`/api/experimental/mcp/servers/{mcpServer}` block: the OAuth2 callback
(its redirect URI is baked into existing AS-side client registrations)
and `oauth2/disconnect` (must remain reachable by users removed from the
organization). These endpoints are under `/api/experimental`, so no
deprecation window is provided.

## Rolling upgrades

During a rolling deploy, an old replica creating an MCP config can fail
the new `NOT NULL organization_id` constraint until it is upgraded
(reads are unaffected: old binaries' generated queries select their own
column lists). This matches the repo's existing precedent for additive
NOT NULL migrations (000562) and affects only the admin config-create
path in the upgrade window.

Upgrades are expected to run in scheduled maintenance downtime with the
database locked during migration, so the migration ships no
rolling-upgrade compatibility machinery. The down migration deletes
organization-created configs (their chat references are cleaned by the
000510 delete trigger) and restores deployment-wide slug uniqueness.

Part of the MCP org-separation stack (CODAGT-711 -> CODAGT-717 audit ->
CODAGT-712 ACLs -> CODAGT-806 token RBAC).

Closes https://linear.app/codercom/issue/CODAGT-711

UAT: validated end to end on a two-org dogfood deployment, including a
real pre-migration to post-migration upgrade, cross-org isolation
(404s), same-slug-two-orgs, chat selection gating, and a live MCP tool
call through the org-scoped generation path. The migration was later
revised to keep existing rows in the default organization only (no
per-organization copies); that revision is covered by the migration test
suite.

> Mux (AI agent) authored this PR on Mike's behalf.

<!-- mux-attribution: model=claude-fable-5 thinking=high -->

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2026-08-19 18:04:13 +00:00

588 lines
17 KiB
Go

// Code generated by typegen/main.go. DO NOT EDIT.
package rbac
import "github.com/coder/coder/v2/coderd/rbac/policy"
// Objecter returns the RBAC object for itself.
type Objecter interface {
RBACObject() Object
}
var (
// ResourceWildcard
// Valid Actions
ResourceWildcard = Object{
Type: "*",
}
// ResourceAIGatewayKey
// Valid Actions
// - "ActionCreate" :: create an AI Gateway key
// - "ActionDelete" :: delete an AI Gateway key
// - "ActionRead" :: read AI Gateway keys
// - "ActionUpdate" :: update an AI Gateway key
ResourceAIGatewayKey = Object{
Type: "ai_gateway_key",
}
// ResourceAiModelPrice
// Valid Actions
// - "ActionRead" :: read AI model prices
// - "ActionUpdate" :: update AI model prices
ResourceAiModelPrice = Object{
Type: "ai_model_price",
}
// ResourceAIProvider
// Valid Actions
// - "ActionCreate" :: create an AI provider
// - "ActionDelete" :: delete an AI provider
// - "ActionRead" :: read AI provider configuration
// - "ActionUpdate" :: update an AI provider
ResourceAIProvider = Object{
Type: "ai_provider",
}
// ResourceAiSeat
// Valid Actions
// - "ActionCreate" :: record AI seat usage
// - "ActionRead" :: read AI seat state
ResourceAiSeat = Object{
Type: "ai_seat",
}
// ResourceAibridgeInterception
// Valid Actions
// - "ActionCreate" :: create aibridge interceptions & related records
// - "ActionRead" :: read aibridge interceptions & related records
// - "ActionUpdate" :: update aibridge interceptions & related records
ResourceAibridgeInterception = Object{
Type: "aibridge_interception",
}
// ResourceApiKey
// Valid Actions
// - "ActionCreate" :: create an api key
// - "ActionDelete" :: delete an api key
// - "ActionRead" :: read api key details (secrets are not stored)
// - "ActionUpdate" :: update an api key, eg expires
ResourceApiKey = Object{
Type: "api_key",
}
// ResourceAssignOrgRole
// Valid Actions
// - "ActionAssign" :: assign org scoped roles
// - "ActionCreate" :: create/delete custom roles within an organization
// - "ActionDelete" :: delete roles within an organization
// - "ActionRead" :: view what roles are assignable within an organization
// - "ActionUnassign" :: unassign org scoped roles
// - "ActionUpdate" :: edit custom roles within an organization
ResourceAssignOrgRole = Object{
Type: "assign_org_role",
}
// ResourceAssignRole
// Valid Actions
// - "ActionAssign" :: assign user roles
// - "ActionRead" :: view what roles are assignable
// - "ActionUnassign" :: unassign user roles
ResourceAssignRole = Object{
Type: "assign_role",
}
// ResourceAuditLog
// Valid Actions
// - "ActionCreate" :: create new audit log entries
// - "ActionRead" :: read audit logs
ResourceAuditLog = Object{
Type: "audit_log",
}
// ResourceBoundaryLog
// Valid Actions
// - "ActionCreate" :: create boundary log records
// - "ActionDelete" :: delete boundary logs
// - "ActionRead" :: read boundary logs and session metadata
ResourceBoundaryLog = Object{
Type: "boundary_log",
}
// ResourceBoundaryUsage
// Valid Actions
// - "ActionDelete" :: delete boundary usage statistics
// - "ActionRead" :: read boundary usage statistics
// - "ActionUpdate" :: upsert boundary usage statistics
ResourceBoundaryUsage = Object{
Type: "boundary_usage",
}
// ResourceChat
// Valid Actions
// - "ActionCreate" :: create a new chat
// - "ActionDelete" :: delete a chat
// - "ActionRead" :: read chat messages and metadata
// - "ActionShare" :: share a chat with other users or groups
// - "ActionUpdate" :: update chat title or settings
ResourceChat = Object{
Type: "chat",
}
// ResourceConnectionLog
// Valid Actions
// - "ActionRead" :: read connection logs
// - "ActionUpdate" :: upsert connection log entries
ResourceConnectionLog = Object{
Type: "connection_log",
}
// ResourceCryptoKey
// Valid Actions
// - "ActionCreate" :: create crypto keys
// - "ActionDelete" :: delete crypto keys
// - "ActionRead" :: read crypto keys
// - "ActionUpdate" :: update crypto keys
ResourceCryptoKey = Object{
Type: "crypto_key",
}
// ResourceDebugInfo
// Valid Actions
// - "ActionRead" :: access to debug routes
ResourceDebugInfo = Object{
Type: "debug_info",
}
// ResourceDeploymentConfig
// Valid Actions
// - "ActionRead" :: read deployment config
// - "ActionUpdate" :: updating health information
ResourceDeploymentConfig = Object{
Type: "deployment_config",
}
// ResourceDeploymentStats
// Valid Actions
// - "ActionRead" :: read deployment stats
ResourceDeploymentStats = Object{
Type: "deployment_stats",
}
// ResourceFile
// Valid Actions
// - "ActionCreate" :: create a file
// - "ActionRead" :: read files
ResourceFile = Object{
Type: "file",
}
// ResourceGroup
// Valid Actions
// - "ActionCreate" :: create a group
// - "ActionDelete" :: delete a group
// - "ActionRead" :: read groups
// - "ActionUpdate" :: update a group
ResourceGroup = Object{
Type: "group",
}
// ResourceGroupMember
// Valid Actions
// - "ActionRead" :: read group members
ResourceGroupMember = Object{
Type: "group_member",
}
// ResourceIdpsyncSettings
// Valid Actions
// - "ActionRead" :: read IdP sync settings
// - "ActionUpdate" :: update IdP sync settings
ResourceIdpsyncSettings = Object{
Type: "idpsync_settings",
}
// ResourceInboxNotification
// Valid Actions
// - "ActionCreate" :: create inbox notifications
// - "ActionRead" :: read inbox notifications
// - "ActionUpdate" :: update inbox notifications
ResourceInboxNotification = Object{
Type: "inbox_notification",
}
// ResourceLicense
// Valid Actions
// - "ActionCreate" :: create a license
// - "ActionDelete" :: delete license
// - "ActionRead" :: read licenses
ResourceLicense = Object{
Type: "license",
}
// ResourceMCPServerConfig
// Valid Actions
// - "ActionCreate" :: create a new MCP server config
// - "ActionDelete" :: delete MCP server config
// - "ActionRead" :: read MCP server config
// - "ActionUpdate" :: update MCP server config
ResourceMCPServerConfig = Object{
Type: "mcp_server_config",
}
// ResourceNotificationMessage
// Valid Actions
// - "ActionCreate" :: create notification messages
// - "ActionDelete" :: delete notification messages
// - "ActionRead" :: read notification messages
// - "ActionUpdate" :: update notification messages
ResourceNotificationMessage = Object{
Type: "notification_message",
}
// ResourceNotificationPreference
// Valid Actions
// - "ActionRead" :: read notification preferences
// - "ActionUpdate" :: update notification preferences
ResourceNotificationPreference = Object{
Type: "notification_preference",
}
// ResourceNotificationTemplate
// Valid Actions
// - "ActionRead" :: read notification templates
// - "ActionUpdate" :: update notification templates
ResourceNotificationTemplate = Object{
Type: "notification_template",
}
// ResourceOauth2App
// Valid Actions
// - "ActionCreate" :: make an OAuth2 app
// - "ActionDelete" :: delete an OAuth2 app
// - "ActionRead" :: read OAuth2 apps
// - "ActionUpdate" :: update the properties of the OAuth2 app
ResourceOauth2App = Object{
Type: "oauth2_app",
}
// ResourceOauth2AppCodeToken
// Valid Actions
// - "ActionCreate" :: create an OAuth2 app code token
// - "ActionDelete" :: delete an OAuth2 app code token
// - "ActionRead" :: read an OAuth2 app code token
ResourceOauth2AppCodeToken = Object{
Type: "oauth2_app_code_token",
}
// ResourceOauth2AppSecret
// Valid Actions
// - "ActionCreate" :: create an OAuth2 app secret
// - "ActionDelete" :: delete an OAuth2 app secret
// - "ActionRead" :: read an OAuth2 app secret
// - "ActionUpdate" :: update an OAuth2 app secret
ResourceOauth2AppSecret = Object{
Type: "oauth2_app_secret",
}
// ResourceOrganization
// Valid Actions
// - "ActionCreate" :: create an organization
// - "ActionDelete" :: delete an organization
// - "ActionRead" :: read organizations
// - "ActionUpdate" :: update an organization
ResourceOrganization = Object{
Type: "organization",
}
// ResourceOrganizationMember
// Valid Actions
// - "ActionCreate" :: create an organization member
// - "ActionDelete" :: delete member
// - "ActionRead" :: read member
// - "ActionUpdate" :: update an organization member
ResourceOrganizationMember = Object{
Type: "organization_member",
}
// ResourcePrebuiltWorkspace
// Valid Actions
// - "ActionDelete" :: delete prebuilt workspace
// - "ActionUpdate" :: update prebuilt workspace settings
ResourcePrebuiltWorkspace = Object{
Type: "prebuilt_workspace",
}
// ResourceProvisionerDaemon
// Valid Actions
// - "ActionCreate" :: create a provisioner daemon/key
// - "ActionDelete" :: delete a provisioner daemon/key
// - "ActionRead" :: read provisioner daemon
// - "ActionUpdate" :: update a provisioner daemon
ResourceProvisionerDaemon = Object{
Type: "provisioner_daemon",
}
// ResourceProvisionerJobs
// Valid Actions
// - "ActionCreate" :: create provisioner jobs
// - "ActionRead" :: read provisioner jobs
// - "ActionUpdate" :: update provisioner jobs
ResourceProvisionerJobs = Object{
Type: "provisioner_jobs",
}
// ResourceReplicas
// Valid Actions
// - "ActionRead" :: read replicas
ResourceReplicas = Object{
Type: "replicas",
}
// ResourceSystem
// Valid Actions
// - "ActionCreate" :: create system resources
// - "ActionDelete" :: delete system resources
// - "ActionRead" :: view system resources
// - "ActionUpdate" :: update system resources
// DEPRECATED: New resources should be created for new things, rather than adding them to System, which has become
// an unmanaged collection of things that don't relate to one another. We can't effectively enforce
// least privilege access control when unrelated resources are grouped together.
ResourceSystem = Object{
Type: "system",
}
// ResourceTailnetCoordinator
// Valid Actions
// - "ActionCreate" :: create a Tailnet coordinator
// - "ActionDelete" :: delete a Tailnet coordinator
// - "ActionRead" :: view info about a Tailnet coordinator
// - "ActionUpdate" :: update a Tailnet coordinator
ResourceTailnetCoordinator = Object{
Type: "tailnet_coordinator",
}
// ResourceTask
// Valid Actions
// - "ActionCreate" :: create a new task
// - "ActionDelete" :: delete task
// - "ActionRead" :: read task data or output to view on the UI or CLI
// - "ActionUpdate" :: edit task settings or send input to an existing task
ResourceTask = Object{
Type: "task",
}
// ResourceTemplate
// Valid Actions
// - "ActionCreate" :: create a template
// - "ActionDelete" :: delete a template
// - "ActionRead" :: read template
// - "ActionUpdate" :: update a template
// - "ActionUse" :: use the template to initially create a workspace, then workspace lifecycle permissions take over
// - "ActionViewInsights" :: view insights
ResourceTemplate = Object{
Type: "template",
}
// ResourceUsageEvent
// Valid Actions
// - "ActionCreate" :: create a usage event
// - "ActionRead" :: read usage events
// - "ActionUpdate" :: update usage events
ResourceUsageEvent = Object{
Type: "usage_event",
}
// ResourceUser
// Valid Actions
// - "ActionCreate" :: create a new user
// - "ActionDelete" :: delete an existing user
// - "ActionRead" :: read user data
// - "ActionReadPersonal" :: read personal user data like user settings and auth links
// - "ActionUpdate" :: update an existing user
// - "ActionUpdatePersonal" :: update personal data
ResourceUser = Object{
Type: "user",
}
// ResourceUserSecret
// Valid Actions
// - "ActionCreate" :: create a user secret
// - "ActionDelete" :: delete a user secret
// - "ActionRead" :: read user secret metadata and value
// - "ActionUpdate" :: update user secret metadata and value
ResourceUserSecret = Object{
Type: "user_secret",
}
// ResourceUserSkill
// Valid Actions
// - "ActionCreate" :: create a user skill
// - "ActionDelete" :: delete a user skill
// - "ActionRead" :: read user skill metadata and content
// - "ActionUpdate" :: update user skill metadata and content
ResourceUserSkill = Object{
Type: "user_skill",
}
// ResourceWebpushSubscription
// Valid Actions
// - "ActionCreate" :: create webpush subscriptions
// - "ActionDelete" :: delete webpush subscriptions
// - "ActionRead" :: read webpush subscriptions
ResourceWebpushSubscription = Object{
Type: "webpush_subscription",
}
// ResourceWorkspace
// Valid Actions
// - "ActionApplicationConnect" :: connect to workspace apps via browser
// - "ActionCreate" :: create a new workspace
// - "ActionCreateAgent" :: create a new workspace agent
// - "ActionDelete" :: delete workspace
// - "ActionDeleteAgent" :: delete an existing workspace agent
// - "ActionRead" :: read workspace data to view on the UI
// - "ActionShare" :: share a workspace with other users or groups
// - "ActionSSH" :: ssh into a given workspace
// - "ActionWorkspaceStart" :: allows starting a workspace
// - "ActionWorkspaceStop" :: allows stopping a workspace
// - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters)
// - "ActionUpdateAgent" :: update an existing workspace agent
ResourceWorkspace = Object{
Type: "workspace",
}
// ResourceWorkspaceAgentDevcontainers
// Valid Actions
// - "ActionCreate" :: create workspace agent devcontainers
ResourceWorkspaceAgentDevcontainers = Object{
Type: "workspace_agent_devcontainers",
}
// ResourceWorkspaceAgentResourceMonitor
// Valid Actions
// - "ActionCreate" :: create workspace agent resource monitor
// - "ActionRead" :: read workspace agent resource monitor
// - "ActionUpdate" :: update workspace agent resource monitor
ResourceWorkspaceAgentResourceMonitor = Object{
Type: "workspace_agent_resource_monitor",
}
// ResourceWorkspaceBuildOrchestration
// Valid Actions
// - "ActionCreate" :: create a workspace build orchestration
// - "ActionDelete" :: delete a workspace build orchestration
// - "ActionRead" :: read a workspace build orchestration
// - "ActionUpdate" :: update a workspace build orchestration
ResourceWorkspaceBuildOrchestration = Object{
Type: "workspace_build_orchestration",
}
// ResourceWorkspaceDormant
// Valid Actions
// - "ActionApplicationConnect" :: connect to workspace apps via browser
// - "ActionCreate" :: create a new workspace
// - "ActionCreateAgent" :: create a new workspace agent
// - "ActionDelete" :: delete workspace
// - "ActionDeleteAgent" :: delete an existing workspace agent
// - "ActionRead" :: read workspace data to view on the UI
// - "ActionShare" :: share a workspace with other users or groups
// - "ActionSSH" :: ssh into a given workspace
// - "ActionWorkspaceStart" :: allows starting a workspace
// - "ActionWorkspaceStop" :: allows stopping a workspace
// - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters)
// - "ActionUpdateAgent" :: update an existing workspace agent
ResourceWorkspaceDormant = Object{
Type: "workspace_dormant",
}
// ResourceWorkspaceProxy
// Valid Actions
// - "ActionCreate" :: create a workspace proxy
// - "ActionDelete" :: delete a workspace proxy
// - "ActionRead" :: read and use a workspace proxy
// - "ActionUpdate" :: update a workspace proxy
ResourceWorkspaceProxy = Object{
Type: "workspace_proxy",
}
)
func AllResources() []Objecter {
return []Objecter{
ResourceWildcard,
ResourceAIGatewayKey,
ResourceAiModelPrice,
ResourceAIProvider,
ResourceAiSeat,
ResourceAibridgeInterception,
ResourceApiKey,
ResourceAssignOrgRole,
ResourceAssignRole,
ResourceAuditLog,
ResourceBoundaryLog,
ResourceBoundaryUsage,
ResourceChat,
ResourceConnectionLog,
ResourceCryptoKey,
ResourceDebugInfo,
ResourceDeploymentConfig,
ResourceDeploymentStats,
ResourceFile,
ResourceGroup,
ResourceGroupMember,
ResourceIdpsyncSettings,
ResourceInboxNotification,
ResourceLicense,
ResourceMCPServerConfig,
ResourceNotificationMessage,
ResourceNotificationPreference,
ResourceNotificationTemplate,
ResourceOauth2App,
ResourceOauth2AppCodeToken,
ResourceOauth2AppSecret,
ResourceOrganization,
ResourceOrganizationMember,
ResourcePrebuiltWorkspace,
ResourceProvisionerDaemon,
ResourceProvisionerJobs,
ResourceReplicas,
ResourceSystem,
ResourceTailnetCoordinator,
ResourceTask,
ResourceTemplate,
ResourceUsageEvent,
ResourceUser,
ResourceUserSecret,
ResourceUserSkill,
ResourceWebpushSubscription,
ResourceWorkspace,
ResourceWorkspaceAgentDevcontainers,
ResourceWorkspaceAgentResourceMonitor,
ResourceWorkspaceBuildOrchestration,
ResourceWorkspaceDormant,
ResourceWorkspaceProxy,
}
}
func AllActions() []policy.Action {
return []policy.Action{
policy.ActionApplicationConnect,
policy.ActionAssign,
policy.ActionCreate,
policy.ActionCreateAgent,
policy.ActionDelete,
policy.ActionDeleteAgent,
policy.ActionRead,
policy.ActionReadPersonal,
policy.ActionSSH,
policy.ActionShare,
policy.ActionUnassign,
policy.ActionUpdate,
policy.ActionUpdateAgent,
policy.ActionUpdatePersonal,
policy.ActionUse,
policy.ActionViewInsights,
policy.ActionWorkspaceStart,
policy.ActionWorkspaceStop,
}
}