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.
This commit is contained in:
Ethan
2026-05-04 01:55:28 +00:00
committed by GitHub
parent 3a153ebb15
commit 761adfa62a
2 changed files with 12 additions and 1 deletions
+2
View File
@@ -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},
+10 -1
View File
@@ -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},