From 761adfa62afc53eb3e538f6d16401d84b003be42 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Mon, 4 May 2026 11:55:28 +1000 Subject: [PATCH] fix(coderd/rbac): grant template admin read access to dormant workspaces (#23554) ## Summary Template admins could **list** dormant workspaces but could not **read** them individually, resulting in a 403 when clicking into a dormant workspace that was visible in the list. ### Root cause - `GetWorkspaces` prepares its SQL authorization filter against the `workspace` type, so dormant workspaces pass the filter and appear in list results for template admins. - `GetWorkspaceByID` calls `RBACObject()` on the fetched workspace, which returns `workspace_dormant` when `DormantAt` is set. Template admin had zero permissions on that type, so the read was denied. ### Fix Add `ActionRead` on `ResourceWorkspaceDormant` to both the site-level `template-admin` and org-level `organization-template-admin` roles. This is the minimal grant needed to make list and read consistent without granting any lifecycle permissions (create, update, delete, stop, etc.) on dormant workspaces. Split the `WorkspaceDormant` RBAC test case into `WorkspaceDormantRead` (read only) and `WorkspaceDormant` (remaining write/lifecycle actions) so the new permission can be asserted independently. Template admins can read non-dormant workspaces, so this is the only missing permission. --- > This PR was generated with Coder agents and reviewed by a human. --- coderd/rbac/roles.go | 2 ++ coderd/rbac/roles_test.go | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 94ca6a875a..c9dc94c300 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -370,6 +370,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { // CRUD all files, even those they did not upload. ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, ResourceWorkspace.Type: {policy.ActionRead}, + ResourceWorkspaceDormant.Type: {policy.ActionRead}, ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, // CRUD to provisioner daemons for now. ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, @@ -532,6 +533,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceTemplate.Type: ResourceTemplate.AvailableActions(), ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, ResourceWorkspace.Type: {policy.ActionRead}, + ResourceWorkspaceDormant.Type: {policy.ActionRead}, ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete}, // Assigning template perms requires this permission. ResourceOrganization.Type: {policy.ActionRead}, diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 212a1d48cd..a59f40461d 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -637,9 +637,18 @@ func TestRolePermissions(t *testing.T) { false: {setOtherOrg, memberMe, agentsAccessUser}, }, }, + { + Name: "WorkspaceDormantRead", + Actions: []policy.Action{policy.ActionRead}, + Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {orgAdmin, owner, templateAdmin, orgTemplateAdmin}, + false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor}, + }, + }, { Name: "WorkspaceDormant", - Actions: append(crud, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent), + Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent}, Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {orgAdmin, owner},