Files
coder/coderd/database/sqlc.yaml
T
Michael Suchacz 9f4ddea571 feat: revoke MCP server OAuth grants at the provider on disconnect (#27300)
Closes
[CODAGT-805](https://linear.app/codercom/issue/CODAGT-805/revoke-oauth-grants-at-the-source-for-mcp-servers).

The experimental MCP server OAuth2 disconnect endpoint previously
deleted only the stored token row, leaving the grant active at the OAuth
provider. This PR adds provider-side token revocation while keeping
local disconnect independent of provider availability.

## Changes

- Add `mcp_server_configs.oauth2_revocation_url` in migration `000547`.
The value can be configured manually, discovered from RFC 8414 metadata,
and managed through the MCP server settings UI. Non-admin responses
redact it with the other OAuth2 fields.
- Revoke the refresh token first through the RFC 7009 endpoint, then
fall back to the access token only for `unsupported_token_type`. Public
clients send `client_id`; confidential clients use
`client_secret_basic`.
- Delete the local token transactionally before best-effort provider
revocation. Callers without a token receive the same response for hidden
and nonexistent config IDs, and provider failures return a generic
warning without exposing provider response bodies.
- Require HTTPS revocation endpoints except for HTTP loopback URLs.
Redirects must preserve the POST and remain on the configured origin.
Redirect errors omit provider-controlled paths and query strings so
reflected token material cannot enter logs.
- Treat `200 OK` and `204 No Content` as completed revocations. `202
Accepted` remains a failure because it does not confirm completion.
- Prevent an in-flight refresh from recreating a token deleted by
disconnect. Refresh persistence now uses an optimistic update keyed by
token ID and `updated_at`; only the OAuth callback can create a token
row. Refresh conflicts reload the current row or clear in-memory auth
when disconnect deleted it.
- Return `{token_revoked, token_revocation_error}` from disconnect,
while retaining SDK compatibility with the legacy `204` response. The UI
surfaces provider revocation failures as warning toasts.
- Document revocation endpoint discovery, HTTPS requirements, and
best-effort disconnect behavior.

No token or no configured revocation URL returns `token_revoked: false`
without an error, so disconnect remains idempotent.

> Updated by Mux, an AI coding agent, on Mike's behalf.
2026-07-20 00:14:03 +02:00

306 lines
12 KiB
YAML

# sqlc is used to generate types from sql schema language.
# It was chosen to ensure type-safety when interacting with
# the database.
version: "2"
cloud:
# This is the static ID for the coder project.
project: "01HEP08N3WKWRFZT3ZZ9Q37J8X"
sql:
- schema: "./dump.sql"
queries: "./queries"
engine: "postgresql"
# This only works if you are running a local postgres database with the
# schema loaded and migrations run. Run `make sqlc-vet` to run the linter.
database:
uri: "${SQLC_DATABASE_URL}"
analyzer:
database: false
rules:
- sqlc/db-prepare
- do-not-use-public-schema-in-queries
gen:
go:
package: "database"
out: "./queries"
emit_interface: true
emit_json_tags: true
emit_db_tags: true
emit_enum_valid_method: true
emit_all_enum_values: true
initialisms:
# Keep the sqlc default initialism and add AI so generated
# identifiers follow Go naming conventions.
- id
- ai
- nats
overrides:
- column: "api_keys.scopes"
go_type:
type: "APIKeyScopes"
- column: "api_keys.allow_list"
go_type:
type: "AllowList"
- db_type: "agent_id_name_pair"
go_type:
type: "AgentIDNamePair"
# Used in 'CustomRoles' query to filter by (name,organization_id)
- db_type: "name_organization_pair"
go_type:
type: "NameOrganizationPair"
- db_type: "tagset"
go_type:
type: "StringMap"
- column: "custom_roles.site_permissions"
go_type:
type: "CustomRolePermissions"
- column: "custom_roles.org_permissions"
go_type:
type: "CustomRolePermissions"
- column: "custom_roles.user_permissions"
go_type:
type: "CustomRolePermissions"
- column: "custom_roles.member_permissions"
go_type:
type: "CustomRolePermissions"
- column: "provisioner_daemons.tags"
go_type:
type: "StringMap"
- column: "provisioner_keys.tags"
go_type:
type: "StringMap"
- column: "provisioner_jobs.tags"
go_type:
type: "StringMap"
- column: "chats.labels"
go_type:
type: "StringMap"
- column: "chats_expanded.labels"
go_type:
type: "StringMap"
- column: "chats.user_acl"
go_type:
type: "ChatACL"
- column: "chats.group_acl"
go_type:
type: "ChatACL"
- column: "chats_expanded.user_acl"
go_type:
type: "ChatACL"
- column: "chats_expanded.group_acl"
go_type:
type: "ChatACL"
- column: "users.rbac_roles"
go_type: "github.com/lib/pq.StringArray"
- column: "templates.user_acl"
go_type:
type: "TemplateACL"
- column: "templates.group_acl"
go_type:
type: "TemplateACL"
- column: "template_with_names.user_acl"
go_type:
type: "TemplateACL"
- column: "template_with_names.group_acl"
go_type:
type: "TemplateACL"
- column: "template_usage_stats.app_usage_mins"
go_type:
type: "StringMapOfInt"
- column: "tasks_with_status.workspace_user_acl"
go_type:
type: "WorkspaceACL"
- column: "tasks_with_status.workspace_group_acl"
go_type:
type: "WorkspaceACL"
- column: "workspaces.user_acl"
go_type:
type: "WorkspaceACL"
- column: "workspaces.group_acl"
go_type:
type: "WorkspaceACL"
- column: "workspaces_expanded.user_acl"
go_type:
type: "WorkspaceACL"
- column: "workspaces_expanded.group_acl"
go_type:
type: "WorkspaceACL"
- column: "workspaces_expanded.user_acl_display_info"
go_type:
type: "WorkspaceACLDisplayInfo"
- column: "workspaces_expanded.group_acl_display_info"
go_type:
type: "WorkspaceACLDisplayInfo"
- column: "notification_templates.actions"
go_type:
type: "[]byte"
- column: "notification_messages.payload"
go_type:
type: "[]byte"
- column: "provisioner_job_stats.*_secs"
go_type:
type: "float64"
- column: "user_links.claims"
go_type:
type: "UserLinkClaims"
# Workaround for sqlc not interpreting the left join correctly.
- column: "tasks_with_status.workspace_build_number"
go_type: "database/sql.NullInt32"
- column: "tasks_with_status.status"
go_type:
type: "TaskStatus"
- column: "tasks_with_status.workspace_agent_lifecycle_state"
go_type:
type: "NullWorkspaceAgentLifecycleState"
- column: "tasks_with_status.workspace_app_health"
go_type:
type: "NullWorkspaceAppHealth"
# Workaround for sqlc not interpreting the left join correctly
# in the combined telemetry query.
- column: "task_event_data.start_build_number"
go_type: "database/sql.NullInt32"
- column: "task_event_data.stop_build_created_at"
go_type: "database/sql.NullTime"
- column: "task_event_data.stop_build_reason"
go_type:
type: "NullBuildReason"
- column: "task_event_data.start_build_created_at"
go_type: "database/sql.NullTime"
- column: "task_event_data.start_build_reason"
go_type:
type: "NullBuildReason"
- column: "task_event_data.last_working_status_at"
go_type: "database/sql.NullTime"
- column: "task_event_data.first_status_after_resume_at"
go_type: "database/sql.NullTime"
- db_type: "pg_catalog.numeric"
go_type:
import: "github.com/shopspring/decimal"
type: "Decimal"
package: "decimal"
- db_type: "pg_catalog.numeric"
nullable: true
go_type:
import: "github.com/shopspring/decimal"
type: "NullDecimal"
package: "decimal"
rename:
ai_provider_id: AIProviderID
chat: ChatTable
chats_expanded: Chat
group_member: GroupMemberTable
group_members_expanded: GroupMember
template: TemplateTable
template_with_name: Template
workspace_build: WorkspaceBuildTable
workspace_build_with_user: WorkspaceBuild
workspace: WorkspaceTable
workspaces_expanded: Workspace
task: TaskTable
tasks_with_status: Task
template_version: TemplateVersionTable
template_version_with_user: TemplateVersion
api_key: APIKey
api_key_scope: APIKeyScope
api_key_scope_all: APIKeyScopeAll
api_key_scope_application_connect: APIKeyScopeApplicationConnect
api_version: APIVersion
avatar_url: AvatarURL
created_by_avatar_url: CreatedByAvatarURL
diff_url: DiffURL
dbcrypt_key: DBCryptKey
session_count_vscode: SessionCountVSCode
session_count_jetbrains: SessionCountJetBrains
session_count_reconnecting_pty: SessionCountReconnectingPTY
session_count_ssh: SessionCountSSH
connection_median_latency_ms: ConnectionMedianLatencyMS
login_type_oidc: LoginTypeOIDC
oauth_access_token: OAuthAccessToken
oauth_access_token_key_id: OAuthAccessTokenKeyID
oauth_expiry: OAuthExpiry
oauth_id_token: OAuthIDToken
oauth_refresh_token: OAuthRefreshToken
oauth_refresh_token_key_id: OAuthRefreshTokenKeyID
oauth_extra: OAuthExtra
parameter_type_system_hcl: ParameterTypeSystemHCL
userstatus: UserStatus
gitsshkey: GitSSHKey
rbac_roles: RBACRoles
ip_address: IPAddress
ip_addresses: IPAddresses
ids: IDs
jwt: JWT
user_acl: UserACL
group_acl: GroupACL
workspace_user_acl: WorkspaceUserACL
workspace_group_acl: WorkspaceGroupACL
user_acl_display_info: UserACLDisplayInfo
group_acl_display_info: GroupACLDisplayInfo
troubleshooting_url: TroubleshootingURL
default_ttl: DefaultTTL
motd_file: MOTDFile
uuid: UUID
failure_ttl: FailureTTL
time_til_dormant_autodelete: TimeTilDormantAutoDelete
eof: EOF
template_ids: TemplateIDs
active_user_ids: ActiveUserIDs
display_app_ssh_helper: DisplayAppSSHHelper
oauth2_provider_app: OAuth2ProviderApp
oauth2_provider_app_secret: OAuth2ProviderAppSecret
oauth2_provider_app_code: OAuth2ProviderAppCode
oauth2_provider_app_token: OAuth2ProviderAppToken
api_key_id: APIKeyID
callback_url: CallbackURL
login_type_oauth2_provider_app: LoginTypeOAuth2ProviderApp
crypto_key_feature_workspace_apps_api_key: CryptoKeyFeatureWorkspaceAppsAPIKey
crypto_key_feature_oidc_convert: CryptoKeyFeatureOIDCConvert
crypto_key_feature_nats_ca: CryptoKeyFeatureNATSCA
stale_interval_ms: StaleIntervalMS
has_ai_task: HasAITask
ai_task_sidebar_app_id: AITaskSidebarAppID
latest_build_has_ai_task: LatestBuildHasAITask
cors_behavior: CorsBehavior
aibridge_interception: AIBridgeInterception
aibridge_interception_error_type: AIBridgeInterceptionErrorType
aibridge_tool_usage: AIBridgeToolUsage
aibridge_token_usage: AIBridgeTokenUsage
aibridge_user_prompt: AIBridgeUserPrompt
aibridge_model_thought: AIBridgeModelThought
ai_provider: AIProvider
ai_provider_key: AIProviderKey
ai_provider_type: AIProviderType
ai_gateway_key: AIGatewayKey
resource_type_ai_provider: ResourceTypeAIProvider
resource_type_ai_provider_key: ResourceTypeAIProviderKey
resource_type_ai_gateway_key: ResourceTypeAIGatewayKey
mcp_server_config: MCPServerConfig
mcp_server_configs: MCPServerConfigs
mcp_server_user_token: MCPServerUserToken
mcp_server_user_tokens: MCPServerUserTokens
mcp_server_tool_snapshot: MCPServerToolSnapshot
mcp_server_tool_snapshots: MCPServerToolSnapshots
mcp_server_config_id: MCPServerConfigID
mcp_server_ids: MCPServerIDs
max_file_links: MaxFileLinks
icon_url: IconURL
oauth2_client_id: OAuth2ClientID
oauth2_client_secret: OAuth2ClientSecret
oauth2_client_secret_key_id: OAuth2ClientSecretKeyID
oauth2_auth_url: OAuth2AuthURL
oauth2_token_url: OAuth2TokenURL
oauth2_revocation_url: OAuth2RevocationURL
oauth2_scopes: OAuth2Scopes
api_key_header: APIKeyHeader
api_key_value: APIKeyValue
api_key_value_key_id: APIKeyValueKeyID
custom_headers_key_id: CustomHeadersKeyID
tools_json: ToolsJSON
access_token_key_id: AccessTokenKeyID
refresh_token_key_id: RefreshTokenKeyID
rules:
- name: do-not-use-public-schema-in-queries
message: "do not use public schema in queries"
# FIXME: It would be great to run sqlc-vet against `migrations` directory and `dump.sql`.
rule: >
query.sql.matches(r'[^a-z]public\.')