From 9cf24fc8c459702f74f8cef443b8b5822dbc1111 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:10:01 +0000 Subject: [PATCH 1/3] feat!: remove the Coder Tasks RBAC resource, scopes and permissions Remove the task RBAC resource and its API key scopes from the authorization catalogs. Strip persisted task permissions and scopes, revoke task-only grants, and preserve restrictive OAuth2 app allow lists. Regenerate the database models, SDK bindings, and API documentation. Add a focused migration round-trip test covering retained permissions and revoked grants. --- coderd/apidoc/docs.go | 12 - coderd/apidoc/swagger.json | 12 - coderd/database/dbauthz/dbauthz.go | 5 +- coderd/database/dump.sql | 5 - .../000594_remove_task_permissions.down.sql | 258 +++++++++++++ .../000594_remove_task_permissions.up.sql | 339 ++++++++++++++++++ coderd/database/migrations/migrate_test.go | 131 +++++++ coderd/database/models.go | 15 - coderd/rbac/object_gen.go | 11 - coderd/rbac/policy/policy.go | 10 - coderd/rbac/roles.go | 2 - coderd/rbac/roles_test.go | 9 - coderd/rbac/scopes.go | 14 +- coderd/rbac/scopes_catalog.go | 7 - coderd/rbac/scopes_constants_gen.go | 12 - codersdk/apikey_scopes_gen.go | 10 - codersdk/rbacresources_gen.go | 2 - docs/manifest.json | 2 +- docs/reference/api/members.md | 40 +-- docs/reference/api/schemas.md | 12 +- docs/reference/api/users.md | 10 +- site/src/api/rbacresourcesGenerated.ts | 6 - site/src/api/typesGenerated.ts | 12 - 23 files changed, 763 insertions(+), 173 deletions(-) create mode 100644 coderd/database/migrations/000594_remove_task_permissions.down.sql create mode 100644 coderd/database/migrations/000594_remove_task_permissions.up.sql diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 72bf693deb020..47561e811ca00 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -18373,11 +18373,6 @@ const docTemplate = `{ "tailnet_coordinator:delete", "tailnet_coordinator:read", "tailnet_coordinator:update", - "task:*", - "task:create", - "task:delete", - "task:read", - "task:update", "template:*", "template:create", "template:delete", @@ -18625,11 +18620,6 @@ const docTemplate = `{ "APIKeyScopeTailnetCoordinatorDelete", "APIKeyScopeTailnetCoordinatorRead", "APIKeyScopeTailnetCoordinatorUpdate", - "APIKeyScopeTaskAll", - "APIKeyScopeTaskCreate", - "APIKeyScopeTaskDelete", - "APIKeyScopeTaskRead", - "APIKeyScopeTaskUpdate", "APIKeyScopeTemplateAll", "APIKeyScopeTemplateCreate", "APIKeyScopeTemplateDelete", @@ -27109,7 +27099,6 @@ const docTemplate = `{ "replicas", "system", "tailnet_coordinator", - "task", "template", "usage_event", "user", @@ -27164,7 +27153,6 @@ const docTemplate = `{ "ResourceReplicas", "ResourceSystem", "ResourceTailnetCoordinator", - "ResourceTask", "ResourceTemplate", "ResourceUsageEvent", "ResourceUser", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 58f7ca844a1fd..e79772c4bcddf 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -16443,11 +16443,6 @@ "tailnet_coordinator:delete", "tailnet_coordinator:read", "tailnet_coordinator:update", - "task:*", - "task:create", - "task:delete", - "task:read", - "task:update", "template:*", "template:create", "template:delete", @@ -16695,11 +16690,6 @@ "APIKeyScopeTailnetCoordinatorDelete", "APIKeyScopeTailnetCoordinatorRead", "APIKeyScopeTailnetCoordinatorUpdate", - "APIKeyScopeTaskAll", - "APIKeyScopeTaskCreate", - "APIKeyScopeTaskDelete", - "APIKeyScopeTaskRead", - "APIKeyScopeTaskUpdate", "APIKeyScopeTemplateAll", "APIKeyScopeTemplateCreate", "APIKeyScopeTemplateDelete", @@ -24861,7 +24851,6 @@ "replicas", "system", "tailnet_coordinator", - "task", "template", "usage_event", "user", @@ -24916,7 +24905,6 @@ "ResourceReplicas", "ResourceSystem", "ResourceTailnetCoordinator", - "ResourceTask", "ResourceTemplate", "ResourceUsageEvent", "ResourceUser", diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 65a5d8c12c496..31c1a3c5beb37 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -258,9 +258,7 @@ var ( rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent}, rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionCreateAgent}, - // Provisionerd needs to read, update, and delete tasks associated with workspaces. - rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - rbac.ResourceApiKey.Type: {policy.WildcardSymbol}, + rbac.ResourceApiKey.Type: {policy.WildcardSymbol}, // When org scoped provisioner credentials are implemented, // this can be reduced to read a specific org. rbac.ResourceOrganization.Type: {policy.ActionRead}, @@ -293,7 +291,6 @@ var ( rbac.ResourceFile.Type: {policy.ActionRead}, // Required to read terraform files rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceSystem.Type: {policy.WildcardSymbol}, - rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceUser.Type: {policy.ActionRead}, rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop}, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index edeae3f00b865..88ee095802011 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -230,11 +230,6 @@ CREATE TYPE api_key_scope AS ENUM ( 'workspace_agent_resource_monitor:*', 'workspace_dormant:*', 'workspace_proxy:*', - 'task:create', - 'task:read', - 'task:update', - 'task:delete', - 'task:*', 'workspace:share', 'workspace_dormant:share', 'boundary_usage:*', diff --git a/coderd/database/migrations/000594_remove_task_permissions.down.sql b/coderd/database/migrations/000594_remove_task_permissions.down.sql new file mode 100644 index 0000000000000..e40567311d723 --- /dev/null +++ b/coderd/database/migrations/000594_remove_task_permissions.down.sql @@ -0,0 +1,258 @@ +-- Restore task scopes in the api_key_scope enum. Revoked keys and grants, +-- stripped scopes, and removed custom role permissions are not recoverable. +ALTER TYPE api_key_scope RENAME TO api_key_scope_old; + +CREATE TYPE api_key_scope AS ENUM ( + 'coder:all', + 'coder:application_connect', + 'aibridge_interception:create', + 'aibridge_interception:read', + 'aibridge_interception:update', + 'api_key:create', + 'api_key:delete', + 'api_key:read', + 'api_key:update', + 'assign_org_role:assign', + 'assign_org_role:create', + 'assign_org_role:delete', + 'assign_org_role:read', + 'assign_org_role:unassign', + 'assign_org_role:update', + 'assign_role:assign', + 'assign_role:read', + 'assign_role:unassign', + 'audit_log:create', + 'audit_log:read', + 'connection_log:read', + 'connection_log:update', + 'crypto_key:create', + 'crypto_key:delete', + 'crypto_key:read', + 'crypto_key:update', + 'debug_info:read', + 'deployment_config:read', + 'deployment_config:update', + 'deployment_stats:read', + 'file:create', + 'file:read', + 'group:create', + 'group:delete', + 'group:read', + 'group:update', + 'group_member:read', + 'idpsync_settings:read', + 'idpsync_settings:update', + 'inbox_notification:create', + 'inbox_notification:read', + 'inbox_notification:update', + 'license:create', + 'license:delete', + 'license:read', + 'notification_message:create', + 'notification_message:delete', + 'notification_message:read', + 'notification_message:update', + 'notification_preference:read', + 'notification_preference:update', + 'notification_template:read', + 'notification_template:update', + 'oauth2_app:create', + 'oauth2_app:delete', + 'oauth2_app:read', + 'oauth2_app:update', + 'oauth2_app_code_token:create', + 'oauth2_app_code_token:delete', + 'oauth2_app_code_token:read', + 'oauth2_app_secret:create', + 'oauth2_app_secret:delete', + 'oauth2_app_secret:read', + 'oauth2_app_secret:update', + 'organization:create', + 'organization:delete', + 'organization:read', + 'organization:update', + 'organization_member:create', + 'organization_member:delete', + 'organization_member:read', + 'organization_member:update', + 'prebuilt_workspace:delete', + 'prebuilt_workspace:update', + 'provisioner_daemon:create', + 'provisioner_daemon:delete', + 'provisioner_daemon:read', + 'provisioner_daemon:update', + 'provisioner_jobs:create', + 'provisioner_jobs:read', + 'provisioner_jobs:update', + 'replicas:read', + 'system:create', + 'system:delete', + 'system:read', + 'system:update', + 'tailnet_coordinator:create', + 'tailnet_coordinator:delete', + 'tailnet_coordinator:read', + 'tailnet_coordinator:update', + 'template:create', + 'template:delete', + 'template:read', + 'template:update', + 'template:use', + 'template:view_insights', + 'usage_event:create', + 'usage_event:read', + 'usage_event:update', + 'user:create', + 'user:delete', + 'user:read', + 'user:read_personal', + 'user:update', + 'user:update_personal', + 'user_secret:create', + 'user_secret:delete', + 'user_secret:read', + 'user_secret:update', + 'webpush_subscription:create', + 'webpush_subscription:delete', + 'webpush_subscription:read', + 'workspace:application_connect', + 'workspace:create', + 'workspace:create_agent', + 'workspace:delete', + 'workspace:delete_agent', + 'workspace:read', + 'workspace:ssh', + 'workspace:start', + 'workspace:stop', + 'workspace:update', + 'workspace_agent_devcontainers:create', + 'workspace_agent_resource_monitor:create', + 'workspace_agent_resource_monitor:read', + 'workspace_agent_resource_monitor:update', + 'workspace_dormant:application_connect', + 'workspace_dormant:create', + 'workspace_dormant:create_agent', + 'workspace_dormant:delete', + 'workspace_dormant:delete_agent', + 'workspace_dormant:read', + 'workspace_dormant:ssh', + 'workspace_dormant:start', + 'workspace_dormant:stop', + 'workspace_dormant:update', + 'workspace_proxy:create', + 'workspace_proxy:delete', + 'workspace_proxy:read', + 'workspace_proxy:update', + 'coder:workspaces.create', + 'coder:workspaces.operate', + 'coder:workspaces.delete', + 'coder:workspaces.access', + 'coder:templates.build', + 'coder:templates.author', + 'coder:apikeys.manage_self', + 'aibridge_interception:*', + 'api_key:*', + 'assign_org_role:*', + 'assign_role:*', + 'audit_log:*', + 'connection_log:*', + 'crypto_key:*', + 'debug_info:*', + 'deployment_config:*', + 'deployment_stats:*', + 'file:*', + 'group:*', + 'group_member:*', + 'idpsync_settings:*', + 'inbox_notification:*', + 'license:*', + 'notification_message:*', + 'notification_preference:*', + 'notification_template:*', + 'oauth2_app:*', + 'oauth2_app_code_token:*', + 'oauth2_app_secret:*', + 'organization:*', + 'organization_member:*', + 'prebuilt_workspace:*', + 'provisioner_daemon:*', + 'provisioner_jobs:*', + 'replicas:*', + 'system:*', + 'tailnet_coordinator:*', + 'template:*', + 'usage_event:*', + 'user:*', + 'user_secret:*', + 'webpush_subscription:*', + 'workspace:*', + 'workspace_agent_devcontainers:*', + 'workspace_agent_resource_monitor:*', + 'workspace_dormant:*', + 'workspace_proxy:*', + 'task:create', + 'task:read', + 'task:update', + 'task:delete', + 'task:*', + 'workspace:share', + 'workspace_dormant:share', + 'boundary_usage:*', + 'boundary_usage:delete', + 'boundary_usage:read', + 'boundary_usage:update', + 'workspace:update_agent', + 'workspace_dormant:update_agent', + 'chat:create', + 'chat:read', + 'chat:update', + 'chat:delete', + 'chat:*', + 'ai_seat:*', + 'ai_seat:create', + 'ai_seat:read', + 'ai_model_price:*', + 'ai_model_price:read', + 'ai_model_price:update', + 'ai_provider:*', + 'ai_provider:create', + 'ai_provider:delete', + 'ai_provider:read', + 'ai_provider:update', + 'chat:share', + 'user_skill:create', + 'user_skill:read', + 'user_skill:update', + 'user_skill:delete', + 'user_skill:*', + 'boundary_log:*', + 'boundary_log:create', + 'boundary_log:delete', + 'boundary_log:read', + 'ai_gateway_key:*', + 'ai_gateway_key:create', + 'ai_gateway_key:delete', + 'ai_gateway_key:read', + 'ai_gateway_key:update', + 'workspace_build_orchestration:*', + 'workspace_build_orchestration:create', + 'workspace_build_orchestration:delete', + 'workspace_build_orchestration:read', + 'workspace_build_orchestration:update', + 'mcp_server_config:*', + 'mcp_server_config:create', + 'mcp_server_config:read', + 'mcp_server_config:update', + 'mcp_server_config:delete', + 'mcp_server_config:share', + 'chat_model_config:*', + 'chat_model_config:create', + 'chat_model_config:read', + 'chat_model_config:update', + 'chat_model_config:delete', + 'chat_model_config:share' +); + +ALTER TABLE api_keys ALTER COLUMN scopes TYPE api_key_scope[] USING (scopes::text[]::api_key_scope[]); + +DROP TYPE api_key_scope_old; diff --git a/coderd/database/migrations/000594_remove_task_permissions.up.sql b/coderd/database/migrations/000594_remove_task_permissions.up.sql new file mode 100644 index 0000000000000..d4ae557348c33 --- /dev/null +++ b/coderd/database/migrations/000594_remove_task_permissions.up.sql @@ -0,0 +1,339 @@ +-- Remove task permissions from custom roles. +UPDATE custom_roles +SET site_permissions = COALESCE(( + SELECT jsonb_agg(elem) + FROM jsonb_array_elements(site_permissions) elem + WHERE elem->>'resource_type' != 'task' +), '[]'::jsonb) +WHERE site_permissions @> '[{"resource_type": "task"}]'; + +UPDATE custom_roles +SET org_permissions = COALESCE(( + SELECT jsonb_agg(elem) + FROM jsonb_array_elements(org_permissions) elem + WHERE elem->>'resource_type' != 'task' +), '[]'::jsonb) +WHERE org_permissions @> '[{"resource_type": "task"}]'; + +UPDATE custom_roles +SET user_permissions = COALESCE(( + SELECT jsonb_agg(elem) + FROM jsonb_array_elements(user_permissions) elem + WHERE elem->>'resource_type' != 'task' +), '[]'::jsonb) +WHERE user_permissions @> '[{"resource_type": "task"}]'; + +UPDATE custom_roles +SET member_permissions = COALESCE(( + SELECT jsonb_agg(elem) + FROM jsonb_array_elements(member_permissions) elem + WHERE elem->>'resource_type' != 'task' +), '[]'::jsonb) +WHERE member_permissions @> '[{"resource_type": "task"}]'; + +-- Remove task scopes from persisted API keys and OAuth2 grants so scope +-- expansion cannot hit the deleted task scope definitions. Keys and grants +-- left with no other scope are revoked outright. +DELETE FROM api_keys +WHERE EXISTS (SELECT 1 FROM unnest(scopes) s WHERE s::text LIKE 'task:%') + AND NOT EXISTS (SELECT 1 FROM unnest(scopes) s WHERE s::text NOT LIKE 'task:%'); + +UPDATE api_keys +SET scopes = ARRAY(SELECT s FROM unnest(scopes) s WHERE s::text NOT LIKE 'task:%') +WHERE EXISTS (SELECT 1 FROM unnest(scopes) s WHERE s::text LIKE 'task:%'); + +-- Remove task entries from API key allow lists; loading a key reparses every +-- entry against the RBAC resource catalog, which no longer knows task. Keys +-- whose allow list only referenced tasks are revoked outright rather than +-- retained with an empty allow list that denies all access. +DELETE FROM api_keys +WHERE NOT EXISTS (SELECT 1 FROM unnest(allow_list) e WHERE e NOT LIKE 'task:%'); + +UPDATE api_keys +SET allow_list = ARRAY(SELECT e FROM unnest(allow_list) e WHERE e NOT LIKE 'task:%') +WHERE EXISTS (SELECT 1 FROM unnest(allow_list) e WHERE e LIKE 'task:%'); + +DELETE FROM oauth2_provider_app_codes +WHERE EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w LIKE 'task:%') + AND NOT EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w NOT LIKE 'task:%' AND w <> ''); + +UPDATE oauth2_provider_app_codes +SET scope = array_to_string(ARRAY(SELECT w FROM unnest(string_to_array(scope, ' ')) w WHERE w NOT LIKE 'task:%' AND w <> ''), ' ') +WHERE EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w LIKE 'task:%'); + +DELETE FROM oauth2_provider_app_tokens +WHERE EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w LIKE 'task:%') + AND NOT EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w NOT LIKE 'task:%' AND w <> ''); + +UPDATE oauth2_provider_app_tokens +SET scope = array_to_string(ARRAY(SELECT w FROM unnest(string_to_array(scope, ' ')) w WHERE w NOT LIKE 'task:%' AND w <> ''), ' ') +WHERE EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w LIKE 'task:%'); + +-- Neutralize apps whose allow list named only task scopes. Stripping to an +-- empty string would read as "no allowlist configured" (unrestricted), so +-- store a whitespace-only allowlist, a configured value that grants nothing. +UPDATE oauth2_provider_apps +SET scope = ' ' +WHERE EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w LIKE 'task:%') + AND NOT EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w NOT LIKE 'task:%' AND w <> ''); + +UPDATE oauth2_provider_apps +SET scope = array_to_string(ARRAY(SELECT w FROM unnest(string_to_array(scope, ' ')) w WHERE w NOT LIKE 'task:%' AND w <> ''), ' ') +WHERE EXISTS (SELECT 1 FROM unnest(string_to_array(scope, ' ')) w WHERE w LIKE 'task:%'); + +-- Remove task scopes from the api_key_scope enum. +ALTER TYPE api_key_scope RENAME TO api_key_scope_old; + +CREATE TYPE api_key_scope AS ENUM ( + 'coder:all', + 'coder:application_connect', + 'aibridge_interception:create', + 'aibridge_interception:read', + 'aibridge_interception:update', + 'api_key:create', + 'api_key:delete', + 'api_key:read', + 'api_key:update', + 'assign_org_role:assign', + 'assign_org_role:create', + 'assign_org_role:delete', + 'assign_org_role:read', + 'assign_org_role:unassign', + 'assign_org_role:update', + 'assign_role:assign', + 'assign_role:read', + 'assign_role:unassign', + 'audit_log:create', + 'audit_log:read', + 'connection_log:read', + 'connection_log:update', + 'crypto_key:create', + 'crypto_key:delete', + 'crypto_key:read', + 'crypto_key:update', + 'debug_info:read', + 'deployment_config:read', + 'deployment_config:update', + 'deployment_stats:read', + 'file:create', + 'file:read', + 'group:create', + 'group:delete', + 'group:read', + 'group:update', + 'group_member:read', + 'idpsync_settings:read', + 'idpsync_settings:update', + 'inbox_notification:create', + 'inbox_notification:read', + 'inbox_notification:update', + 'license:create', + 'license:delete', + 'license:read', + 'notification_message:create', + 'notification_message:delete', + 'notification_message:read', + 'notification_message:update', + 'notification_preference:read', + 'notification_preference:update', + 'notification_template:read', + 'notification_template:update', + 'oauth2_app:create', + 'oauth2_app:delete', + 'oauth2_app:read', + 'oauth2_app:update', + 'oauth2_app_code_token:create', + 'oauth2_app_code_token:delete', + 'oauth2_app_code_token:read', + 'oauth2_app_secret:create', + 'oauth2_app_secret:delete', + 'oauth2_app_secret:read', + 'oauth2_app_secret:update', + 'organization:create', + 'organization:delete', + 'organization:read', + 'organization:update', + 'organization_member:create', + 'organization_member:delete', + 'organization_member:read', + 'organization_member:update', + 'prebuilt_workspace:delete', + 'prebuilt_workspace:update', + 'provisioner_daemon:create', + 'provisioner_daemon:delete', + 'provisioner_daemon:read', + 'provisioner_daemon:update', + 'provisioner_jobs:create', + 'provisioner_jobs:read', + 'provisioner_jobs:update', + 'replicas:read', + 'system:create', + 'system:delete', + 'system:read', + 'system:update', + 'tailnet_coordinator:create', + 'tailnet_coordinator:delete', + 'tailnet_coordinator:read', + 'tailnet_coordinator:update', + 'template:create', + 'template:delete', + 'template:read', + 'template:update', + 'template:use', + 'template:view_insights', + 'usage_event:create', + 'usage_event:read', + 'usage_event:update', + 'user:create', + 'user:delete', + 'user:read', + 'user:read_personal', + 'user:update', + 'user:update_personal', + 'user_secret:create', + 'user_secret:delete', + 'user_secret:read', + 'user_secret:update', + 'webpush_subscription:create', + 'webpush_subscription:delete', + 'webpush_subscription:read', + 'workspace:application_connect', + 'workspace:create', + 'workspace:create_agent', + 'workspace:delete', + 'workspace:delete_agent', + 'workspace:read', + 'workspace:ssh', + 'workspace:start', + 'workspace:stop', + 'workspace:update', + 'workspace_agent_devcontainers:create', + 'workspace_agent_resource_monitor:create', + 'workspace_agent_resource_monitor:read', + 'workspace_agent_resource_monitor:update', + 'workspace_dormant:application_connect', + 'workspace_dormant:create', + 'workspace_dormant:create_agent', + 'workspace_dormant:delete', + 'workspace_dormant:delete_agent', + 'workspace_dormant:read', + 'workspace_dormant:ssh', + 'workspace_dormant:start', + 'workspace_dormant:stop', + 'workspace_dormant:update', + 'workspace_proxy:create', + 'workspace_proxy:delete', + 'workspace_proxy:read', + 'workspace_proxy:update', + 'coder:workspaces.create', + 'coder:workspaces.operate', + 'coder:workspaces.delete', + 'coder:workspaces.access', + 'coder:templates.build', + 'coder:templates.author', + 'coder:apikeys.manage_self', + 'aibridge_interception:*', + 'api_key:*', + 'assign_org_role:*', + 'assign_role:*', + 'audit_log:*', + 'connection_log:*', + 'crypto_key:*', + 'debug_info:*', + 'deployment_config:*', + 'deployment_stats:*', + 'file:*', + 'group:*', + 'group_member:*', + 'idpsync_settings:*', + 'inbox_notification:*', + 'license:*', + 'notification_message:*', + 'notification_preference:*', + 'notification_template:*', + 'oauth2_app:*', + 'oauth2_app_code_token:*', + 'oauth2_app_secret:*', + 'organization:*', + 'organization_member:*', + 'prebuilt_workspace:*', + 'provisioner_daemon:*', + 'provisioner_jobs:*', + 'replicas:*', + 'system:*', + 'tailnet_coordinator:*', + 'template:*', + 'usage_event:*', + 'user:*', + 'user_secret:*', + 'webpush_subscription:*', + 'workspace:*', + 'workspace_agent_devcontainers:*', + 'workspace_agent_resource_monitor:*', + 'workspace_dormant:*', + 'workspace_proxy:*', + 'workspace:share', + 'workspace_dormant:share', + 'boundary_usage:*', + 'boundary_usage:delete', + 'boundary_usage:read', + 'boundary_usage:update', + 'workspace:update_agent', + 'workspace_dormant:update_agent', + 'chat:create', + 'chat:read', + 'chat:update', + 'chat:delete', + 'chat:*', + 'ai_seat:*', + 'ai_seat:create', + 'ai_seat:read', + 'ai_model_price:*', + 'ai_model_price:read', + 'ai_model_price:update', + 'ai_provider:*', + 'ai_provider:create', + 'ai_provider:delete', + 'ai_provider:read', + 'ai_provider:update', + 'chat:share', + 'user_skill:create', + 'user_skill:read', + 'user_skill:update', + 'user_skill:delete', + 'user_skill:*', + 'boundary_log:*', + 'boundary_log:create', + 'boundary_log:delete', + 'boundary_log:read', + 'ai_gateway_key:*', + 'ai_gateway_key:create', + 'ai_gateway_key:delete', + 'ai_gateway_key:read', + 'ai_gateway_key:update', + 'workspace_build_orchestration:*', + 'workspace_build_orchestration:create', + 'workspace_build_orchestration:delete', + 'workspace_build_orchestration:read', + 'workspace_build_orchestration:update', + 'mcp_server_config:*', + 'mcp_server_config:create', + 'mcp_server_config:read', + 'mcp_server_config:update', + 'mcp_server_config:delete', + 'mcp_server_config:share', + 'chat_model_config:*', + 'chat_model_config:create', + 'chat_model_config:read', + 'chat_model_config:update', + 'chat_model_config:delete', + 'chat_model_config:share' +); + +ALTER TABLE api_keys ALTER COLUMN scopes TYPE api_key_scope[] USING (scopes::text[]::api_key_scope[]); + +DROP TYPE api_key_scope_old; + +-- Task audit history is retained: audit logs are subject to retention +-- policies, so the task value stays in the resource_type enum as a +-- historical audit value. diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go index 19457e9ce9f37..d9e90a99d220d 100644 --- a/coderd/database/migrations/migrate_test.go +++ b/coderd/database/migrations/migrate_test.go @@ -1461,6 +1461,137 @@ func TestMigration000593TaskBuildReasonRewriteTiming(t *testing.T) { require.Zero(t, count) } +func TestMigration000594RemoveTaskPermissions(t *testing.T) { + t.Parallel() + + const migrationVersion = 594 + sqlDB := testSQLDB(t) + next, err := migrations.Stepper(sqlDB) + require.NoError(t, err) + for { + version, more, err := next() + require.NoError(t, err) + if !more { + t.Fatalf("migration %d not found", migrationVersion) + } + if version == migrationVersion-1 { + break + } + } + + ctx := testutil.Context(t, testutil.WaitLong) + now := time.Now().UTC().Truncate(time.Microsecond) + orgID, userID, roleID, appID, taskAppID := uuid.New(), uuid.New(), uuid.New(), uuid.New(), uuid.New() + const mixedPermissions = `[{"negate": false, "resource_type": "task", "action": "read"}, {"negate": false, "resource_type": "user", "action": "read"}]` + fixtures := []struct { + query string + args []any + }{ + { + "INSERT INTO organizations (id, name, display_name, description, created_at, updated_at, default_org_member_roles) VALUES ($1, $2, '', '', $3, $3, '{}')", + []any{orgID, orgID.String(), now}, + }, + { + "INSERT INTO users (id, username, email, hashed_password, created_at, updated_at) VALUES ($1, $2, $3, '', $4, $4)", + []any{userID, userID.String(), userID.String() + "@test.com", now}, + }, + { + `INSERT INTO custom_roles (id, name, display_name, organization_id, site_permissions, org_permissions, user_permissions, member_permissions) + VALUES ($1, 'task-role', 'Task Role', $2, $3, '[{"negate": false, "resource_type": "task", "action": "create"}]', $3, $3)`, + []any{roleID, orgID, mixedPermissions}, + }, + { + "INSERT INTO oauth2_provider_apps (id, created_at, updated_at, name, icon, callback_url, scope) VALUES ($1, $3, $3, 'mixed-app', '', 'http://localhost/callback', 'task:read template:read'), ($2, $3, $3, 'task-app', '', 'http://localhost/callback', 'task:read')", + []any{appID, taskAppID, now}, + }, + } + for _, fixture := range fixtures { + _, err := sqlDB.ExecContext(ctx, fixture.query, fixture.args...) + require.NoError(t, err) + } + + for _, key := range []struct{ id, scopes, allowList, loginType string }{ + {"mixed-key", "{task:read,template:read}", "{*:*}", "token"}, + {"task-key", "{task:read}", "{*:*}", "token"}, + {"mixed-allow-key", "{template:read}", "{task:*,user:*}", "token"}, + {"task-allow-key", "{template:read}", "{task:*}", "token"}, + // Non-task key scopes exercise token revocation rather than API key cascades. + {"oauth-mixed-key", "{template:read}", "{*:*}", "oauth2_provider_app"}, + {"oauth-task-key", "{template:read}", "{*:*}", "oauth2_provider_app"}, + } { + _, err := sqlDB.ExecContext(ctx, + "INSERT INTO api_keys (id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, scopes, allow_list, token_name) VALUES ($1, $2, $3, $4, $4, $4, $4, $5, $6, $7, $1)", + key.id, []byte(key.id), userID, now, key.loginType, key.scopes, key.allowList) + require.NoError(t, err) + } + for _, grant := range []struct{ key, scope string }{ + {"oauth-mixed-key", "task:read template:read"}, + {"oauth-task-key", "task:read"}, + } { + _, err := sqlDB.ExecContext(ctx, + "INSERT INTO oauth2_provider_app_codes (id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, scope) VALUES ($1, $2, $2, $3, $3, $4, $5, $6)", + uuid.New(), now, []byte(grant.key), userID, appID, grant.scope) + require.NoError(t, err) + _, err = sqlDB.ExecContext(ctx, + "INSERT INTO oauth2_provider_app_tokens (id, created_at, expires_at, hash_prefix, refresh_hash, api_key_id, user_id, app_id, scope) VALUES ($1, $2, $2, $3, $3, $4, $5, $6, $7)", + uuid.New(), now, []byte(grant.key), grant.key, userID, appID, grant.scope) + require.NoError(t, err) + } + + version, _, err := next() + require.NoError(t, err) + require.EqualValues(t, migrationVersion, version) + assertPermissions := func() { + t.Helper() + var site, org, user, member string + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT site_permissions, org_permissions, user_permissions, member_permissions FROM custom_roles WHERE id = $1", roleID).Scan(&site, &org, &user, &member)) + const userPermission = `[{"negate": false, "resource_type": "user", "action": "read"}]` + require.JSONEq(t, userPermission, site) + require.JSONEq(t, "[]", org) + require.JSONEq(t, userPermission, user) + require.JSONEq(t, userPermission, member) + var value string + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT scopes::text FROM api_keys WHERE id = 'mixed-key'").Scan(&value)) + require.Equal(t, "{template:read}", value) + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT allow_list::text FROM api_keys WHERE id = 'mixed-allow-key'").Scan(&value)) + require.Equal(t, "{user:*}", value) + var count int + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT count(*) FROM api_keys WHERE id IN ('task-key', 'task-allow-key', 'oauth-task-key')").Scan(&count)) + require.Zero(t, count) + for _, table := range []string{"oauth2_provider_app_codes", "oauth2_provider_app_tokens"} { + var scopes pq.StringArray + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT array_agg(scope) FROM "+table+" WHERE app_id = $1", appID).Scan(&scopes)) + require.Equal(t, pq.StringArray{"template:read"}, scopes) + } + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT scope FROM oauth2_provider_apps WHERE id = $1", taskAppID).Scan(&value)) + require.Equal(t, " ", value) + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT scope FROM oauth2_provider_apps WHERE id = $1", appID).Scan(&value)) + require.Equal(t, "template:read", value) + } + assertPermissions() + const enumQuery = "SELECT count(*) FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'api_key_scope' AND e.enumlabel LIKE 'task:%'" + var taskScopes int + require.NoError(t, sqlDB.QueryRowContext(ctx, enumQuery).Scan(&taskScopes)) + require.Zero(t, taskScopes) + + downSQL, err := os.ReadFile("000594_remove_task_permissions.down.sql") + require.NoError(t, err) + _, err = sqlDB.ExecContext(ctx, string(downSQL)) + require.NoError(t, err) + require.NoError(t, sqlDB.QueryRowContext(ctx, enumQuery).Scan(&taskScopes)) + require.Equal(t, 5, taskScopes) + // Downgrading cannot restore revoked grants or stripped permissions. + assertPermissions() + + upSQL, err := os.ReadFile("000594_remove_task_permissions.up.sql") + require.NoError(t, err) + _, err = sqlDB.ExecContext(ctx, string(upSQL)) + require.NoError(t, err) + assertPermissions() + require.NoError(t, sqlDB.QueryRowContext(ctx, enumQuery).Scan(&taskScopes)) + require.Zero(t, taskScopes) +} + func TestMigration000587RemoveAgentsAccessRole(t *testing.T) { t.Parallel() diff --git a/coderd/database/models.go b/coderd/database/models.go index 91b561d91d990..29ea53e0216ec 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -474,11 +474,6 @@ const ( ApiKeyScopeWorkspaceAgentResourceMonitor APIKeyScope = "workspace_agent_resource_monitor:*" ApiKeyScopeWorkspaceDormant APIKeyScope = "workspace_dormant:*" ApiKeyScopeWorkspaceProxy APIKeyScope = "workspace_proxy:*" - ApiKeyScopeTaskCreate APIKeyScope = "task:create" - ApiKeyScopeTaskRead APIKeyScope = "task:read" - ApiKeyScopeTaskUpdate APIKeyScope = "task:update" - ApiKeyScopeTaskDelete APIKeyScope = "task:delete" - ApiKeyScopeTask APIKeyScope = "task:*" ApiKeyScopeWorkspaceShare APIKeyScope = "workspace:share" ApiKeyScopeWorkspaceDormantShare APIKeyScope = "workspace_dormant:share" ApiKeyScopeBoundaryUsage APIKeyScope = "boundary_usage:*" @@ -761,11 +756,6 @@ func (e APIKeyScope) Valid() bool { ApiKeyScopeWorkspaceAgentResourceMonitor, ApiKeyScopeWorkspaceDormant, ApiKeyScopeWorkspaceProxy, - ApiKeyScopeTaskCreate, - ApiKeyScopeTaskRead, - ApiKeyScopeTaskUpdate, - ApiKeyScopeTaskDelete, - ApiKeyScopeTask, ApiKeyScopeWorkspaceShare, ApiKeyScopeWorkspaceDormantShare, ApiKeyScopeBoundaryUsage, @@ -1016,11 +1006,6 @@ func AllAPIKeyScopeValues() []APIKeyScope { ApiKeyScopeWorkspaceAgentResourceMonitor, ApiKeyScopeWorkspaceDormant, ApiKeyScopeWorkspaceProxy, - ApiKeyScopeTaskCreate, - ApiKeyScopeTaskRead, - ApiKeyScopeTaskUpdate, - ApiKeyScopeTaskDelete, - ApiKeyScopeTask, ApiKeyScopeWorkspaceShare, ApiKeyScopeWorkspaceDormantShare, ApiKeyScopeBoundaryUsage, diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index c1e4312500216..843c785dd8d53 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -373,16 +373,6 @@ var ( 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 @@ -560,7 +550,6 @@ func AllResources() []Objecter { ResourceReplicas, ResourceSystem, ResourceTailnetCoordinator, - ResourceTask, ResourceTemplate, ResourceUsageEvent, ResourceUser, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 067c42ee0f410..9ddc2235614fb 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -70,13 +70,6 @@ var workspaceActions = map[Action]ActionDefinition{ ActionShare: "share a workspace with other users or groups", } -var taskActions = map[Action]ActionDefinition{ - ActionCreate: "create a new 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", - ActionDelete: "delete task", -} - var chatActions = map[Action]ActionDefinition{ ActionCreate: "create a new chat", ActionRead: "read chat messages and metadata", @@ -124,9 +117,6 @@ var RBACPermissions = map[string]PermissionDefinition{ "workspace": { Actions: workspaceActions, }, - "task": { - Actions: taskActions, - }, "chat": { Actions: chatActions, }, diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 163a5e93efbb6..1d0d0648ed477 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -250,8 +250,6 @@ func orgWorkspaceAccessMemberPerms() []Permission { // stay dead at Member scope. ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead}, - ResourceTask.Type: ResourceTask.AvailableActions(), - // Intentionally omitted at Member scope (resources without an // Owner field on their RBACObject; Member-level grants never // fire for them). Listed here because these can be common diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 9029236c3ff3d..abbff4a4bc71f 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -781,15 +781,6 @@ func TestRolePermissions(t *testing.T) { false: {setOtherOrg, userAdmin, memberMe, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, - { - Name: "Task", - Actions: crud, - Resource: rbac.ResourceTask.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), - AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, orgWorkspaceAccessUser}, - false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor}, - }, - }, // Some admin style resources { Name: "Licenses", diff --git a/coderd/rbac/scopes.go b/coderd/rbac/scopes.go index 98a2417bd8bc6..b0beaf4cc004a 100644 --- a/coderd/rbac/scopes.go +++ b/coderd/rbac/scopes.go @@ -16,7 +16,6 @@ type WorkspaceAgentScopeParams struct { OwnerID uuid.UUID TemplateID uuid.UUID VersionID uuid.UUID - TaskID uuid.NullUUID BlockUserData bool } @@ -41,15 +40,6 @@ func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope { panic("failed to expand scope, this should never happen") } - // Include task in the allow list if the workspace has an associated task. - var extraAllowList []AllowListElement - if params.TaskID.Valid { - extraAllowList = append(extraAllowList, AllowListElement{ - Type: ResourceTask.Type, - ID: params.TaskID.UUID.String(), - }) - } - return Scope{ // TODO: We want to limit the role too to be extra safe. // Even though the allowlist blocks anything else, it is still good @@ -60,7 +50,7 @@ func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope { // Limit the agent to only be able to access the singular workspace and // the template/version it was created from. Add additional resources here // as needed, but do not add more workspace or template resource ids. - AllowIDList: append([]AllowListElement{ + AllowIDList: []AllowListElement{ {Type: ResourceWorkspace.Type, ID: params.WorkspaceID.String()}, {Type: ResourceTemplate.Type, ID: params.TemplateID.String()}, {Type: ResourceTemplate.Type, ID: params.VersionID.String()}, @@ -70,7 +60,7 @@ func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope { // logs. Adding site-level actions to the member role would // bypass this and grant deployment-wide access. {Type: ResourceBoundaryLog.Type, ID: policy.WildcardSymbol}, - }, extraAllowList...), + }, } } diff --git a/coderd/rbac/scopes_catalog.go b/coderd/rbac/scopes_catalog.go index ca496eb485bfa..e2b40c4ecf572 100644 --- a/coderd/rbac/scopes_catalog.go +++ b/coderd/rbac/scopes_catalog.go @@ -64,13 +64,6 @@ var externalLowLevel = map[ScopeName]struct{}{ "user_skill:delete": {}, "user_skill:*": {}, - // Tasks - "task:create": {}, - "task:read": {}, - "task:update": {}, - "task:delete": {}, - "task:*": {}, - // Organizations "organization:read": {}, "organization:update": {}, diff --git a/coderd/rbac/scopes_constants_gen.go b/coderd/rbac/scopes_constants_gen.go index aa8f95fbfc42e..cd03f54492fc9 100644 --- a/coderd/rbac/scopes_constants_gen.go +++ b/coderd/rbac/scopes_constants_gen.go @@ -128,10 +128,6 @@ const ( ScopeTailnetCoordinatorDelete ScopeName = "tailnet_coordinator:delete" ScopeTailnetCoordinatorRead ScopeName = "tailnet_coordinator:read" ScopeTailnetCoordinatorUpdate ScopeName = "tailnet_coordinator:update" - ScopeTaskCreate ScopeName = "task:create" - ScopeTaskDelete ScopeName = "task:delete" - ScopeTaskRead ScopeName = "task:read" - ScopeTaskUpdate ScopeName = "task:update" ScopeTemplateCreate ScopeName = "template:create" ScopeTemplateDelete ScopeName = "template:delete" ScopeTemplateRead ScopeName = "template:read" @@ -326,10 +322,6 @@ func (e ScopeName) Valid() bool { ScopeTailnetCoordinatorDelete, ScopeTailnetCoordinatorRead, ScopeTailnetCoordinatorUpdate, - ScopeTaskCreate, - ScopeTaskDelete, - ScopeTaskRead, - ScopeTaskUpdate, ScopeTemplateCreate, ScopeTemplateDelete, ScopeTemplateRead, @@ -525,10 +517,6 @@ func AllScopeNameValues() []ScopeName { ScopeTailnetCoordinatorDelete, ScopeTailnetCoordinatorRead, ScopeTailnetCoordinatorUpdate, - ScopeTaskCreate, - ScopeTaskDelete, - ScopeTaskRead, - ScopeTaskUpdate, ScopeTemplateCreate, ScopeTemplateDelete, ScopeTemplateRead, diff --git a/codersdk/apikey_scopes_gen.go b/codersdk/apikey_scopes_gen.go index 13cbd38416a03..83cb93f897102 100644 --- a/codersdk/apikey_scopes_gen.go +++ b/codersdk/apikey_scopes_gen.go @@ -175,11 +175,6 @@ const ( APIKeyScopeTailnetCoordinatorDelete APIKeyScope = "tailnet_coordinator:delete" APIKeyScopeTailnetCoordinatorRead APIKeyScope = "tailnet_coordinator:read" APIKeyScopeTailnetCoordinatorUpdate APIKeyScope = "tailnet_coordinator:update" - APIKeyScopeTaskAll APIKeyScope = "task:*" - APIKeyScopeTaskCreate APIKeyScope = "task:create" - APIKeyScopeTaskDelete APIKeyScope = "task:delete" - APIKeyScopeTaskRead APIKeyScope = "task:read" - APIKeyScopeTaskUpdate APIKeyScope = "task:update" APIKeyScopeTemplateAll APIKeyScope = "template:*" APIKeyScopeTemplateCreate APIKeyScope = "template:create" APIKeyScopeTemplateDelete APIKeyScope = "template:delete" @@ -281,11 +276,6 @@ var PublicAPIKeyScopes = []APIKeyScope{ APIKeyScopeOrganizationDelete, APIKeyScopeOrganizationRead, APIKeyScopeOrganizationUpdate, - APIKeyScopeTaskAll, - APIKeyScopeTaskCreate, - APIKeyScopeTaskDelete, - APIKeyScopeTaskRead, - APIKeyScopeTaskUpdate, APIKeyScopeTemplateAll, APIKeyScopeTemplateCreate, APIKeyScopeTemplateDelete, diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 4614e43942ef5..e1b79a8cbcae8 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -44,7 +44,6 @@ const ( ResourceReplicas RBACResource = "replicas" ResourceSystem RBACResource = "system" ResourceTailnetCoordinator RBACResource = "tailnet_coordinator" - ResourceTask RBACResource = "task" ResourceTemplate RBACResource = "template" ResourceUsageEvent RBACResource = "usage_event" ResourceUser RBACResource = "user" @@ -125,7 +124,6 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceReplicas: {ActionRead}, ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceTailnetCoordinator: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, - ResourceTask: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionUse, ActionViewInsights}, ResourceUsageEvent: {ActionCreate, ActionRead, ActionUpdate}, ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal}, diff --git a/docs/manifest.json b/docs/manifest.json index cfd5eadc64f17..1987f5c461172 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1130,7 +1130,7 @@ }, { "title": "Tasks to Chats API Migration", - "description": "Migrate your integrations from the deprecated Coder Tasks API to the Coder Agents Chats API.", + "description": "Migrate your integrations from the removed Coder Tasks API to the Coder Agents Chats API.", "path": "./ai-coder/agents/tasks-to-chats-migration.md" } ] diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 35c55be05a08f..b09f3004a7821 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -198,10 +198,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -331,10 +331,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -464,10 +464,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -559,10 +559,10 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -965,9 +965,9 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | -| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | +| Property | Value(s) | +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` | +| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index ce95802e3e050..3544f5c75bc99 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1331,9 +1331,9 @@ None #### Enumerated Values -| Value(s) | -|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `ai_gateway_key:*`, `ai_gateway_key:create`, `ai_gateway_key:delete`, `ai_gateway_key:read`, `ai_gateway_key:update`, `ai_model_price:*`, `ai_model_price:read`, `ai_model_price:update`, `ai_provider:*`, `ai_provider:create`, `ai_provider:delete`, `ai_provider:read`, `ai_provider:update`, `ai_seat:*`, `ai_seat:create`, `ai_seat:read`, `aibridge_interception:*`, `aibridge_interception:create`, `aibridge_interception:read`, `aibridge_interception:update`, `all`, `api_key:*`, `api_key:create`, `api_key:delete`, `api_key:read`, `api_key:update`, `application_connect`, `assign_org_role:*`, `assign_org_role:assign`, `assign_org_role:create`, `assign_org_role:delete`, `assign_org_role:read`, `assign_org_role:unassign`, `assign_org_role:update`, `assign_role:*`, `assign_role:assign`, `assign_role:read`, `assign_role:unassign`, `audit_log:*`, `audit_log:create`, `audit_log:read`, `boundary_log:*`, `boundary_log:create`, `boundary_log:delete`, `boundary_log:read`, `boundary_usage:*`, `boundary_usage:delete`, `boundary_usage:read`, `boundary_usage:update`, `chat:*`, `chat:create`, `chat:delete`, `chat:read`, `chat:share`, `chat:update`, `chat_model_config:*`, `chat_model_config:create`, `chat_model_config:delete`, `chat_model_config:read`, `chat_model_config:share`, `chat_model_config:update`, `coder:all`, `coder:apikeys.manage_self`, `coder:application_connect`, `coder:templates.author`, `coder:templates.build`, `coder:workspaces.access`, `coder:workspaces.create`, `coder:workspaces.delete`, `coder:workspaces.operate`, `connection_log:*`, `connection_log:read`, `connection_log:update`, `crypto_key:*`, `crypto_key:create`, `crypto_key:delete`, `crypto_key:read`, `crypto_key:update`, `debug_info:*`, `debug_info:read`, `deployment_config:*`, `deployment_config:read`, `deployment_config:update`, `deployment_stats:*`, `deployment_stats:read`, `file:*`, `file:create`, `file:read`, `group:*`, `group:create`, `group:delete`, `group:read`, `group:update`, `group_member:*`, `group_member:read`, `idpsync_settings:*`, `idpsync_settings:read`, `idpsync_settings:update`, `inbox_notification:*`, `inbox_notification:create`, `inbox_notification:read`, `inbox_notification:update`, `license:*`, `license:create`, `license:delete`, `license:read`, `mcp_server_config:*`, `mcp_server_config:create`, `mcp_server_config:delete`, `mcp_server_config:read`, `mcp_server_config:share`, `mcp_server_config:update`, `notification_message:*`, `notification_message:create`, `notification_message:delete`, `notification_message:read`, `notification_message:update`, `notification_preference:*`, `notification_preference:read`, `notification_preference:update`, `notification_template:*`, `notification_template:read`, `notification_template:update`, `oauth2_app:*`, `oauth2_app:create`, `oauth2_app:delete`, `oauth2_app:read`, `oauth2_app:update`, `oauth2_app_code_token:*`, `oauth2_app_code_token:create`, `oauth2_app_code_token:delete`, `oauth2_app_code_token:read`, `oauth2_app_secret:*`, `oauth2_app_secret:create`, `oauth2_app_secret:delete`, `oauth2_app_secret:read`, `oauth2_app_secret:update`, `organization:*`, `organization:create`, `organization:delete`, `organization:read`, `organization:update`, `organization_member:*`, `organization_member:create`, `organization_member:delete`, `organization_member:read`, `organization_member:update`, `prebuilt_workspace:*`, `prebuilt_workspace:delete`, `prebuilt_workspace:update`, `provisioner_daemon:*`, `provisioner_daemon:create`, `provisioner_daemon:delete`, `provisioner_daemon:read`, `provisioner_daemon:update`, `provisioner_jobs:*`, `provisioner_jobs:create`, `provisioner_jobs:read`, `provisioner_jobs:update`, `replicas:*`, `replicas:read`, `system:*`, `system:create`, `system:delete`, `system:read`, `system:update`, `tailnet_coordinator:*`, `tailnet_coordinator:create`, `tailnet_coordinator:delete`, `tailnet_coordinator:read`, `tailnet_coordinator:update`, `task:*`, `task:create`, `task:delete`, `task:read`, `task:update`, `template:*`, `template:create`, `template:delete`, `template:read`, `template:update`, `template:use`, `template:view_insights`, `usage_event:*`, `usage_event:create`, `usage_event:read`, `usage_event:update`, `user:*`, `user:create`, `user:delete`, `user:read`, `user:read_personal`, `user:update`, `user:update_personal`, `user_secret:*`, `user_secret:create`, `user_secret:delete`, `user_secret:read`, `user_secret:update`, `user_skill:*`, `user_skill:create`, `user_skill:delete`, `user_skill:read`, `user_skill:update`, `webpush_subscription:*`, `webpush_subscription:create`, `webpush_subscription:delete`, `webpush_subscription:read`, `workspace:*`, `workspace:application_connect`, `workspace:create`, `workspace:create_agent`, `workspace:delete`, `workspace:delete_agent`, `workspace:read`, `workspace:share`, `workspace:ssh`, `workspace:start`, `workspace:stop`, `workspace:update`, `workspace:update_agent`, `workspace_agent_devcontainers:*`, `workspace_agent_devcontainers:create`, `workspace_agent_resource_monitor:*`, `workspace_agent_resource_monitor:create`, `workspace_agent_resource_monitor:read`, `workspace_agent_resource_monitor:update`, `workspace_build_orchestration:*`, `workspace_build_orchestration:create`, `workspace_build_orchestration:delete`, `workspace_build_orchestration:read`, `workspace_build_orchestration:update`, `workspace_dormant:*`, `workspace_dormant:application_connect`, `workspace_dormant:create`, `workspace_dormant:create_agent`, `workspace_dormant:delete`, `workspace_dormant:delete_agent`, `workspace_dormant:read`, `workspace_dormant:share`, `workspace_dormant:ssh`, `workspace_dormant:start`, `workspace_dormant:stop`, `workspace_dormant:update`, `workspace_dormant:update_agent`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` | +| Value(s) | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ai_gateway_key:*`, `ai_gateway_key:create`, `ai_gateway_key:delete`, `ai_gateway_key:read`, `ai_gateway_key:update`, `ai_model_price:*`, `ai_model_price:read`, `ai_model_price:update`, `ai_provider:*`, `ai_provider:create`, `ai_provider:delete`, `ai_provider:read`, `ai_provider:update`, `ai_seat:*`, `ai_seat:create`, `ai_seat:read`, `aibridge_interception:*`, `aibridge_interception:create`, `aibridge_interception:read`, `aibridge_interception:update`, `all`, `api_key:*`, `api_key:create`, `api_key:delete`, `api_key:read`, `api_key:update`, `application_connect`, `assign_org_role:*`, `assign_org_role:assign`, `assign_org_role:create`, `assign_org_role:delete`, `assign_org_role:read`, `assign_org_role:unassign`, `assign_org_role:update`, `assign_role:*`, `assign_role:assign`, `assign_role:read`, `assign_role:unassign`, `audit_log:*`, `audit_log:create`, `audit_log:read`, `boundary_log:*`, `boundary_log:create`, `boundary_log:delete`, `boundary_log:read`, `boundary_usage:*`, `boundary_usage:delete`, `boundary_usage:read`, `boundary_usage:update`, `chat:*`, `chat:create`, `chat:delete`, `chat:read`, `chat:share`, `chat:update`, `chat_model_config:*`, `chat_model_config:create`, `chat_model_config:delete`, `chat_model_config:read`, `chat_model_config:share`, `chat_model_config:update`, `coder:all`, `coder:apikeys.manage_self`, `coder:application_connect`, `coder:templates.author`, `coder:templates.build`, `coder:workspaces.access`, `coder:workspaces.create`, `coder:workspaces.delete`, `coder:workspaces.operate`, `connection_log:*`, `connection_log:read`, `connection_log:update`, `crypto_key:*`, `crypto_key:create`, `crypto_key:delete`, `crypto_key:read`, `crypto_key:update`, `debug_info:*`, `debug_info:read`, `deployment_config:*`, `deployment_config:read`, `deployment_config:update`, `deployment_stats:*`, `deployment_stats:read`, `file:*`, `file:create`, `file:read`, `group:*`, `group:create`, `group:delete`, `group:read`, `group:update`, `group_member:*`, `group_member:read`, `idpsync_settings:*`, `idpsync_settings:read`, `idpsync_settings:update`, `inbox_notification:*`, `inbox_notification:create`, `inbox_notification:read`, `inbox_notification:update`, `license:*`, `license:create`, `license:delete`, `license:read`, `mcp_server_config:*`, `mcp_server_config:create`, `mcp_server_config:delete`, `mcp_server_config:read`, `mcp_server_config:share`, `mcp_server_config:update`, `notification_message:*`, `notification_message:create`, `notification_message:delete`, `notification_message:read`, `notification_message:update`, `notification_preference:*`, `notification_preference:read`, `notification_preference:update`, `notification_template:*`, `notification_template:read`, `notification_template:update`, `oauth2_app:*`, `oauth2_app:create`, `oauth2_app:delete`, `oauth2_app:read`, `oauth2_app:update`, `oauth2_app_code_token:*`, `oauth2_app_code_token:create`, `oauth2_app_code_token:delete`, `oauth2_app_code_token:read`, `oauth2_app_secret:*`, `oauth2_app_secret:create`, `oauth2_app_secret:delete`, `oauth2_app_secret:read`, `oauth2_app_secret:update`, `organization:*`, `organization:create`, `organization:delete`, `organization:read`, `organization:update`, `organization_member:*`, `organization_member:create`, `organization_member:delete`, `organization_member:read`, `organization_member:update`, `prebuilt_workspace:*`, `prebuilt_workspace:delete`, `prebuilt_workspace:update`, `provisioner_daemon:*`, `provisioner_daemon:create`, `provisioner_daemon:delete`, `provisioner_daemon:read`, `provisioner_daemon:update`, `provisioner_jobs:*`, `provisioner_jobs:create`, `provisioner_jobs:read`, `provisioner_jobs:update`, `replicas:*`, `replicas:read`, `system:*`, `system:create`, `system:delete`, `system:read`, `system:update`, `tailnet_coordinator:*`, `tailnet_coordinator:create`, `tailnet_coordinator:delete`, `tailnet_coordinator:read`, `tailnet_coordinator:update`, `template:*`, `template:create`, `template:delete`, `template:read`, `template:update`, `template:use`, `template:view_insights`, `usage_event:*`, `usage_event:create`, `usage_event:read`, `usage_event:update`, `user:*`, `user:create`, `user:delete`, `user:read`, `user:read_personal`, `user:update`, `user:update_personal`, `user_secret:*`, `user_secret:create`, `user_secret:delete`, `user_secret:read`, `user_secret:update`, `user_skill:*`, `user_skill:create`, `user_skill:delete`, `user_skill:read`, `user_skill:update`, `webpush_subscription:*`, `webpush_subscription:create`, `webpush_subscription:delete`, `webpush_subscription:read`, `workspace:*`, `workspace:application_connect`, `workspace:create`, `workspace:create_agent`, `workspace:delete`, `workspace:delete_agent`, `workspace:read`, `workspace:share`, `workspace:ssh`, `workspace:start`, `workspace:stop`, `workspace:update`, `workspace:update_agent`, `workspace_agent_devcontainers:*`, `workspace_agent_devcontainers:create`, `workspace_agent_resource_monitor:*`, `workspace_agent_resource_monitor:create`, `workspace_agent_resource_monitor:read`, `workspace_agent_resource_monitor:update`, `workspace_build_orchestration:*`, `workspace_build_orchestration:create`, `workspace_build_orchestration:delete`, `workspace_build_orchestration:read`, `workspace_build_orchestration:update`, `workspace_dormant:*`, `workspace_dormant:application_connect`, `workspace_dormant:create`, `workspace_dormant:create_agent`, `workspace_dormant:delete`, `workspace_dormant:delete_agent`, `workspace_dormant:read`, `workspace_dormant:share`, `workspace_dormant:ssh`, `workspace_dormant:start`, `workspace_dormant:stop`, `workspace_dormant:update`, `workspace_dormant:update_agent`, `workspace_proxy:*`, `workspace_proxy:create`, `workspace_proxy:delete`, `workspace_proxy:read`, `workspace_proxy:update` | ## codersdk.AddLicenseRequest @@ -12894,9 +12894,9 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith #### Enumerated Values -| Value(s) | -|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | +| Value(s) | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | ## codersdk.RateLimitConfig diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index f3a3a4468307c..c458ef590b2de 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -870,11 +870,11 @@ Status Code **200** #### Enumerated Values -| Property | Value(s) | -|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | -| `login_type` | `github`, `oidc`, `password`, `token` | -| `scope` | `all`, `application_connect` | +| Property | Value(s) | +|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` | +| `login_type` | `github`, `oidc`, `password`, `token` | +| `scope` | `all`, `application_connect` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index 0911c135fe105..a7d02193938b3 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -207,12 +207,6 @@ export const RBACResourceActions: Partial< read: "view info about a Tailnet coordinator", update: "update a Tailnet coordinator", }, - task: { - create: "create a new task", - delete: "delete task", - read: "read task data or output to view on the UI or CLI", - update: "edit task settings or send input to an existing task", - }, template: { create: "create a template", delete: "delete a template", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0856644d1468c..eda6d27844dfd 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -772,11 +772,6 @@ export type APIKeyScope = | "tailnet_coordinator:delete" | "tailnet_coordinator:read" | "tailnet_coordinator:update" - | "task:*" - | "task:create" - | "task:delete" - | "task:read" - | "task:update" | "template:*" | "template:create" | "template:delete" @@ -1024,11 +1019,6 @@ export const APIKeyScopes: APIKeyScope[] = [ "tailnet_coordinator:delete", "tailnet_coordinator:read", "tailnet_coordinator:update", - "task:*", - "task:create", - "task:delete", - "task:read", - "task:update", "template:*", "template:create", "template:delete", @@ -7882,7 +7872,6 @@ export type RBACResource = | "replicas" | "system" | "tailnet_coordinator" - | "task" | "template" | "usage_event" | "user" @@ -7937,7 +7926,6 @@ export const RBACResources: RBACResource[] = [ "replicas", "system", "tailnet_coordinator", - "task", "template", "usage_event", "user", From 7f5b2f8a4a18af3e0e935596097e3b845d3f276a Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:12:09 +0000 Subject: [PATCH 2/3] fix(coderd/database/migrations): simplify task permission cleanup and restructure its test --- ...> 000595_remove_task_permissions.down.sql} | 0 ... => 000595_remove_task_permissions.up.sql} | 8 +- coderd/database/migrations/migrate_test.go | 179 +++++++++++------- 3 files changed, 116 insertions(+), 71 deletions(-) rename coderd/database/migrations/{000594_remove_task_permissions.down.sql => 000595_remove_task_permissions.down.sql} (100%) rename coderd/database/migrations/{000594_remove_task_permissions.up.sql => 000595_remove_task_permissions.up.sql} (97%) diff --git a/coderd/database/migrations/000594_remove_task_permissions.down.sql b/coderd/database/migrations/000595_remove_task_permissions.down.sql similarity index 100% rename from coderd/database/migrations/000594_remove_task_permissions.down.sql rename to coderd/database/migrations/000595_remove_task_permissions.down.sql diff --git a/coderd/database/migrations/000594_remove_task_permissions.up.sql b/coderd/database/migrations/000595_remove_task_permissions.up.sql similarity index 97% rename from coderd/database/migrations/000594_remove_task_permissions.up.sql rename to coderd/database/migrations/000595_remove_task_permissions.up.sql index d4ae557348c33..e841a0aae1cb5 100644 --- a/coderd/database/migrations/000594_remove_task_permissions.up.sql +++ b/coderd/database/migrations/000595_remove_task_permissions.up.sql @@ -35,17 +35,15 @@ WHERE member_permissions @> '[{"resource_type": "task"}]'; -- expansion cannot hit the deleted task scope definitions. Keys and grants -- left with no other scope are revoked outright. DELETE FROM api_keys -WHERE EXISTS (SELECT 1 FROM unnest(scopes) s WHERE s::text LIKE 'task:%') - AND NOT EXISTS (SELECT 1 FROM unnest(scopes) s WHERE s::text NOT LIKE 'task:%'); +WHERE NOT EXISTS (SELECT 1 FROM unnest(scopes) s WHERE s::text NOT LIKE 'task:%'); UPDATE api_keys SET scopes = ARRAY(SELECT s FROM unnest(scopes) s WHERE s::text NOT LIKE 'task:%') WHERE EXISTS (SELECT 1 FROM unnest(scopes) s WHERE s::text LIKE 'task:%'); -- Remove task entries from API key allow lists; loading a key reparses every --- entry against the RBAC resource catalog, which no longer knows task. Keys --- whose allow list only referenced tasks are revoked outright rather than --- retained with an empty allow list that denies all access. +-- entry against the RBAC resource catalog, which no longer knows task. The +-- allow list cannot be empty, so keys that only referenced tasks are revoked. DELETE FROM api_keys WHERE NOT EXISTS (SELECT 1 FROM unnest(allow_list) e WHERE e NOT LIKE 'task:%'); diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go index d9e90a99d220d..23e93760b58e1 100644 --- a/coderd/database/migrations/migrate_test.go +++ b/coderd/database/migrations/migrate_test.go @@ -1461,10 +1461,10 @@ func TestMigration000593TaskBuildReasonRewriteTiming(t *testing.T) { require.Zero(t, count) } -func TestMigration000594RemoveTaskPermissions(t *testing.T) { +func TestMigration000595RemoveTaskPermissions(t *testing.T) { t.Parallel() - const migrationVersion = 594 + const migrationVersion = 595 sqlDB := testSQLDB(t) next, err := migrations.Stepper(sqlDB) require.NoError(t, err) @@ -1482,8 +1482,21 @@ func TestMigration000594RemoveTaskPermissions(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) now := time.Now().UTC().Truncate(time.Microsecond) orgID, userID, roleID, appID, taskAppID := uuid.New(), uuid.New(), uuid.New(), uuid.New(), uuid.New() - const mixedPermissions = `[{"negate": false, "resource_type": "task", "action": "read"}, {"negate": false, "resource_type": "user", "action": "read"}]` - fixtures := []struct { + const ( + taskPermission = `{"negate": false, "resource_type": "task", "action": "read"}` + userPermission = `{"negate": false, "resource_type": "user", "action": "read"}` + ) + permissions := []struct { + column string + before string + want string + }{ + {"site_permissions", "[" + taskPermission + ", " + userPermission + "]", "[" + userPermission + "]"}, + {"org_permissions", "[" + taskPermission + "]", "[]"}, + {"user_permissions", "[" + userPermission + ", " + taskPermission + "]", "[" + userPermission + "]"}, + {"member_permissions", "[" + userPermission + "]", "[" + userPermission + "]"}, + } + for _, fixture := range []struct { query string args []any }{ @@ -1496,38 +1509,66 @@ func TestMigration000594RemoveTaskPermissions(t *testing.T) { []any{userID, userID.String(), userID.String() + "@test.com", now}, }, { - `INSERT INTO custom_roles (id, name, display_name, organization_id, site_permissions, org_permissions, user_permissions, member_permissions) - VALUES ($1, 'task-role', 'Task Role', $2, $3, '[{"negate": false, "resource_type": "task", "action": "create"}]', $3, $3)`, - []any{roleID, orgID, mixedPermissions}, + "INSERT INTO custom_roles (id, name, display_name, organization_id, site_permissions, org_permissions, user_permissions, member_permissions) VALUES ($1, 'task-role', 'Task Role', $2, $3, $4, $5, $6)", + []any{roleID, orgID, permissions[0].before, permissions[1].before, permissions[2].before, permissions[3].before}, }, - { - "INSERT INTO oauth2_provider_apps (id, created_at, updated_at, name, icon, callback_url, scope) VALUES ($1, $3, $3, 'mixed-app', '', 'http://localhost/callback', 'task:read template:read'), ($2, $3, $3, 'task-app', '', 'http://localhost/callback', 'task:read')", - []any{appID, taskAppID, now}, - }, - } - for _, fixture := range fixtures { + } { _, err := sqlDB.ExecContext(ctx, fixture.query, fixture.args...) require.NoError(t, err) } - for _, key := range []struct{ id, scopes, allowList, loginType string }{ - {"mixed-key", "{task:read,template:read}", "{*:*}", "token"}, - {"task-key", "{task:read}", "{*:*}", "token"}, - {"mixed-allow-key", "{template:read}", "{task:*,user:*}", "token"}, - {"task-allow-key", "{template:read}", "{task:*}", "token"}, - // Non-task key scopes exercise token revocation rather than API key cascades. - {"oauth-mixed-key", "{template:read}", "{*:*}", "oauth2_provider_app"}, - {"oauth-task-key", "{template:read}", "{*:*}", "oauth2_provider_app"}, - } { + apps := []struct { + id uuid.UUID + name string + scope string + wantScope string + }{ + {appID, "mixed-app", "task:read template:read", "template:read"}, + // An empty scope reads as unrestricted, so a whitespace-only one stands in for "nothing allowed". + {taskAppID, "task-app", "task:read", " "}, + } + for _, app := range apps { + _, err := sqlDB.ExecContext(ctx, + "INSERT INTO oauth2_provider_apps (id, created_at, updated_at, name, icon, callback_url, scope) VALUES ($1, $2, $2, $3, '', 'http://localhost/callback', $4)", + app.id, now, app.name, app.scope) + require.NoError(t, err) + } + + keys := []struct { + id string + scopes string + allowList string + loginType string + wantDeleted bool + wantScopes string + wantAllowList string + }{ + {id: "mixed-key", scopes: "{task:read,template:read}", allowList: "{*:*}", loginType: "token", wantScopes: "{template:read}", wantAllowList: "{*:*}"}, + {id: "task-key", scopes: "{task:read}", allowList: "{*:*}", loginType: "token", wantDeleted: true}, + {id: "mixed-allow-key", scopes: "{template:read}", allowList: "{task:*,user:*}", loginType: "token", wantScopes: "{template:read}", wantAllowList: "{user:*}"}, + {id: "task-allow-key", scopes: "{template:read}", allowList: "{task:*}", loginType: "token", wantDeleted: true}, + // OAuth keys hold no task scopes themselves; deleting a task-only token revokes its key through the token trigger. + {id: "oauth-mixed-key", scopes: "{template:read}", allowList: "{*:*}", loginType: "oauth2_provider_app", wantScopes: "{template:read}", wantAllowList: "{*:*}"}, + {id: "oauth-task-key", scopes: "{template:read}", allowList: "{*:*}", loginType: "oauth2_provider_app", wantDeleted: true}, + } + for _, key := range keys { _, err := sqlDB.ExecContext(ctx, "INSERT INTO api_keys (id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, scopes, allow_list, token_name) VALUES ($1, $2, $3, $4, $4, $4, $4, $5, $6, $7, $1)", key.id, []byte(key.id), userID, now, key.loginType, key.scopes, key.allowList) require.NoError(t, err) } - for _, grant := range []struct{ key, scope string }{ - {"oauth-mixed-key", "task:read template:read"}, - {"oauth-task-key", "task:read"}, - } { + + // Each grant is stored as both an authorization code and a token, keyed by its API key name. + grants := []struct { + key string + scope string + wantDeleted bool + wantScope string + }{ + {key: "oauth-mixed-key", scope: "task:read template:read", wantScope: "template:read"}, + {key: "oauth-task-key", scope: "task:read", wantDeleted: true}, + } + for _, grant := range grants { _, err := sqlDB.ExecContext(ctx, "INSERT INTO oauth2_provider_app_codes (id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, scope) VALUES ($1, $2, $2, $3, $3, $4, $5, $6)", uuid.New(), now, []byte(grant.key), userID, appID, grant.scope) @@ -1537,59 +1578,65 @@ func TestMigration000594RemoveTaskPermissions(t *testing.T) { uuid.New(), now, []byte(grant.key), grant.key, userID, appID, grant.scope) require.NoError(t, err) } + grantTables := []struct{ name, prefixColumn string }{ + {"oauth2_provider_app_codes", "secret_prefix"}, + {"oauth2_provider_app_tokens", "hash_prefix"}, + } - version, _, err := next() - require.NoError(t, err) - require.EqualValues(t, migrationVersion, version) - assertPermissions := func() { + assertMigrated := func() { t.Helper() - var site, org, user, member string - require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT site_permissions, org_permissions, user_permissions, member_permissions FROM custom_roles WHERE id = $1", roleID).Scan(&site, &org, &user, &member)) - const userPermission = `[{"negate": false, "resource_type": "user", "action": "read"}]` - require.JSONEq(t, userPermission, site) - require.JSONEq(t, "[]", org) - require.JSONEq(t, userPermission, user) - require.JSONEq(t, userPermission, member) - var value string - require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT scopes::text FROM api_keys WHERE id = 'mixed-key'").Scan(&value)) - require.Equal(t, "{template:read}", value) - require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT allow_list::text FROM api_keys WHERE id = 'mixed-allow-key'").Scan(&value)) - require.Equal(t, "{user:*}", value) - var count int - require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT count(*) FROM api_keys WHERE id IN ('task-key', 'task-allow-key', 'oauth-task-key')").Scan(&count)) - require.Zero(t, count) - for _, table := range []string{"oauth2_provider_app_codes", "oauth2_provider_app_tokens"} { - var scopes pq.StringArray - require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT array_agg(scope) FROM "+table+" WHERE app_id = $1", appID).Scan(&scopes)) - require.Equal(t, pq.StringArray{"template:read"}, scopes) + for _, p := range permissions { + var got string + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT "+p.column+" FROM custom_roles WHERE id = $1", roleID).Scan(&got)) + require.JSONEq(t, p.want, got, p.column) + } + for _, app := range apps { + var scope string + require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT scope FROM oauth2_provider_apps WHERE id = $1", app.id).Scan(&scope)) + require.Equal(t, app.wantScope, scope, app.name) + } + for _, key := range keys { + var scopes, allowList string + err := sqlDB.QueryRowContext(ctx, "SELECT scopes::text, allow_list::text FROM api_keys WHERE id = $1", key.id).Scan(&scopes, &allowList) + if key.wantDeleted { + require.ErrorIs(t, err, sql.ErrNoRows, key.id) + continue + } + require.NoError(t, err, key.id) + require.Equal(t, key.wantScopes, scopes, key.id) + require.Equal(t, key.wantAllowList, allowList, key.id) + } + for _, grant := range grants { + for _, table := range grantTables { + var scope string + err := sqlDB.QueryRowContext(ctx, "SELECT scope FROM "+table.name+" WHERE "+table.prefixColumn+" = $1", []byte(grant.key)).Scan(&scope) + if grant.wantDeleted { + require.ErrorIs(t, err, sql.ErrNoRows, "%s %s", table.name, grant.key) + continue + } + require.NoError(t, err, "%s %s", table.name, grant.key) + require.Equal(t, grant.wantScope, scope, "%s %s", table.name, grant.key) + } } - require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT scope FROM oauth2_provider_apps WHERE id = $1", taskAppID).Scan(&value)) - require.Equal(t, " ", value) - require.NoError(t, sqlDB.QueryRowContext(ctx, "SELECT scope FROM oauth2_provider_apps WHERE id = $1", appID).Scan(&value)) - require.Equal(t, "template:read", value) } - assertPermissions() - const enumQuery = "SELECT count(*) FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'api_key_scope' AND e.enumlabel LIKE 'task:%'" - var taskScopes int - require.NoError(t, sqlDB.QueryRowContext(ctx, enumQuery).Scan(&taskScopes)) - require.Zero(t, taskScopes) - downSQL, err := os.ReadFile("000594_remove_task_permissions.down.sql") + version, _, err := next() + require.NoError(t, err) + require.EqualValues(t, migrationVersion, version) + assertMigrated() + + downSQL, err := os.ReadFile("000595_remove_task_permissions.down.sql") require.NoError(t, err) _, err = sqlDB.ExecContext(ctx, string(downSQL)) require.NoError(t, err) - require.NoError(t, sqlDB.QueryRowContext(ctx, enumQuery).Scan(&taskScopes)) - require.Equal(t, 5, taskScopes) // Downgrading cannot restore revoked grants or stripped permissions. - assertPermissions() + assertMigrated() - upSQL, err := os.ReadFile("000594_remove_task_permissions.up.sql") + upSQL, err := os.ReadFile("000595_remove_task_permissions.up.sql") require.NoError(t, err) _, err = sqlDB.ExecContext(ctx, string(upSQL)) require.NoError(t, err) - assertPermissions() - require.NoError(t, sqlDB.QueryRowContext(ctx, enumQuery).Scan(&taskScopes)) - require.Zero(t, taskScopes) + assertMigrated() } func TestMigration000587RemoveAgentsAccessRole(t *testing.T) { From 80ee30141ee81287a03de3f46407f6a421387778 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:12:54 +0000 Subject: [PATCH 3/3] test(coderd/database/migrations): cover multi-scope task keys and simplify grant fixtures --- coderd/database/migrations/migrate_test.go | 30 ++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go index 23e93760b58e1..8ecef244ccefa 100644 --- a/coderd/database/migrations/migrate_test.go +++ b/coderd/database/migrations/migrate_test.go @@ -1538,41 +1538,45 @@ func TestMigration000595RemoveTaskPermissions(t *testing.T) { id string scopes string allowList string - loginType string wantDeleted bool wantScopes string wantAllowList string }{ - {id: "mixed-key", scopes: "{task:read,template:read}", allowList: "{*:*}", loginType: "token", wantScopes: "{template:read}", wantAllowList: "{*:*}"}, - {id: "task-key", scopes: "{task:read}", allowList: "{*:*}", loginType: "token", wantDeleted: true}, - {id: "mixed-allow-key", scopes: "{template:read}", allowList: "{task:*,user:*}", loginType: "token", wantScopes: "{template:read}", wantAllowList: "{user:*}"}, - {id: "task-allow-key", scopes: "{template:read}", allowList: "{task:*}", loginType: "token", wantDeleted: true}, - // OAuth keys hold no task scopes themselves; deleting a task-only token revokes its key through the token trigger. - {id: "oauth-mixed-key", scopes: "{template:read}", allowList: "{*:*}", loginType: "oauth2_provider_app", wantScopes: "{template:read}", wantAllowList: "{*:*}"}, - {id: "oauth-task-key", scopes: "{template:read}", allowList: "{*:*}", loginType: "oauth2_provider_app", wantDeleted: true}, + {id: "mixed-key", scopes: "{task:read,template:read}", allowList: "{*:*}", wantScopes: "{template:read}", wantAllowList: "{*:*}"}, + {id: "multi-task-scopes-key", scopes: "{task:read,task:create,template:read}", allowList: "{*:*}", wantScopes: "{template:read}", wantAllowList: "{*:*}"}, + {id: "task-key", scopes: "{task:read}", allowList: "{*:*}", wantDeleted: true}, + {id: "mixed-allow-key", scopes: "{template:read}", allowList: "{task:*,user:*}", wantScopes: "{template:read}", wantAllowList: "{user:*}"}, + {id: "multi-task-allows-key", scopes: "{template:read}", allowList: "{task:*,task:3f0c9b2e-5d41-4a7b-9c1e-8d2f6a4b7c5e,user:*}", wantScopes: "{template:read}", wantAllowList: "{user:*}"}, + {id: "task-allow-key", scopes: "{template:read}", allowList: "{task:*}", wantDeleted: true}, + {id: "task-only-key", scopes: "{task:read,task:create}", allowList: "{task:*,task:3f0c9b2e-5d41-4a7b-9c1e-8d2f6a4b7c5e}", wantDeleted: true}, } for _, key := range keys { _, err := sqlDB.ExecContext(ctx, - "INSERT INTO api_keys (id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, scopes, allow_list, token_name) VALUES ($1, $2, $3, $4, $4, $4, $4, $5, $6, $7, $1)", - key.id, []byte(key.id), userID, now, key.loginType, key.scopes, key.allowList) + "INSERT INTO api_keys (id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, scopes, allow_list, token_name) VALUES ($1, $2, $3, $4, $4, $4, $4, 'token', $5, $6, $1)", + key.id, []byte(key.id), userID, now, key.scopes, key.allowList) require.NoError(t, err) } - // Each grant is stored as both an authorization code and a token, keyed by its API key name. + // Each grant is stored as both an authorization code and a token, keyed by the grant name. grants := []struct { key string scope string wantDeleted bool wantScope string }{ - {key: "oauth-mixed-key", scope: "task:read template:read", wantScope: "template:read"}, - {key: "oauth-task-key", scope: "task:read", wantDeleted: true}, + {key: "mixed-grant", scope: "task:read template:read", wantScope: "template:read"}, + {key: "task-grant", scope: "task:read", wantDeleted: true}, } for _, grant := range grants { _, err := sqlDB.ExecContext(ctx, "INSERT INTO oauth2_provider_app_codes (id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, scope) VALUES ($1, $2, $2, $3, $3, $4, $5, $6)", uuid.New(), now, []byte(grant.key), userID, appID, grant.scope) require.NoError(t, err) + // oauth2_provider_app_tokens.api_key_id is a NOT NULL foreign key, so this key only satisfies it and is not asserted. + _, err = sqlDB.ExecContext(ctx, + "INSERT INTO api_keys (id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, scopes, allow_list, token_name) VALUES ($1, $2, $3, $4, $4, $4, $4, 'oauth2_provider_app', '{template:read}', '{*:*}', $1)", + grant.key, []byte(grant.key), userID, now) + require.NoError(t, err) _, err = sqlDB.ExecContext(ctx, "INSERT INTO oauth2_provider_app_tokens (id, created_at, expires_at, hash_prefix, refresh_hash, api_key_id, user_id, app_id, scope) VALUES ($1, $2, $2, $3, $3, $4, $5, $6, $7)", uuid.New(), now, []byte(grant.key), grant.key, userID, appID, grant.scope)