mirror of
https://github.com/coder/coder.git
synced 2026-10-31 02:43:21 +08:00
Adds enterprise audit logging for MCP server config create, update, and
delete, with strict secret redaction. MCP configs hold credentials
(OAuth2 client secrets, API keys, custom headers), so admin changes to
them need an audit trail.
## Summary
- `enterprise/audit/table.go` gains an `MCPServerConfig` entry
enumerating every column: `oauth2_client_secret`, `api_key_value`, and
`custom_headers` are `ActionSecret` (never appear in diffs); dbcrypt
`*_key_id` bookkeeping, IDs, and timestamps are ignored; the remaining
config fields, including the endpoint URL fields, are tracked so
auditors can see which endpoints a config points at.
- Type registration in `coderd/audit` (diff, request, resource target
with org attribution), `codersdk/audit.go`, and a `resource_type` enum
migration.
- Handlers wire `audit.InitRequest`: create records `New`; update and
delete record `Old` from the param middleware before the
write-authorization check, so a readable-but-not-writable caller
produces an audited 403 while read-denied callers stay concealed as
unaudited 404s.
- Tests: create/update/delete audit entries, write-denied and
delete-denied 403 auditing, cross-org concealment producing zero
entries, and a serializer-level regression test proving none of the
three secret classes can reach a serialized diff.
- Review round: MCP config audit entries link to
`/ai/settings/mcp-servers/{id}`, audit table comments are trimmed per
review, and a fault-injection test pins that a config row surviving a
failed post-discovery credential update still gets its creation audit
entry.
Stacked on #27942 (org-scoped MCP configs). Part of the MCP
org-separation stack.
Closes https://linear.app/codercom/issue/CODAGT-717
UAT: verified on a trial-licensed dogfood instance: audit entries for
the full CRUD lifecycle with correct actor/org/target, redacted secrets
in the update and OAuth2 create diffs, and a full plaintext scan of the
audit dump finding zero secret leaks.
> Mux (AI agent) authored this PR on Mike's behalf.
<!-- mux-attribution: model=claude-fable-5 thinking=high -->
307 lines
12 KiB
YAML
307 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
|
|
resource_type_mcp_server_config: ResourceTypeMCPServerConfig
|
|
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\.')
|