Skip to content

Commit 13dd79b

Browse files
committed
workspace dormancy
1 parent 5ce4225 commit 13dd79b

File tree

3 files changed

+60
-31
lines changed

3 files changed

+60
-31
lines changed

coderd/rbac/policy/policy.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ func (a ActionDefinition) Requires() string {
8282
return strings.Join(fields, ",")
8383
}
8484

85+
var workspaceActions = map[Action]ActionDefinition{
86+
ActionCreate: actDef(fieldOwner|fieldOrg, "create a new workspace"),
87+
ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data to view on the UI"),
88+
// TODO: Make updates more granular
89+
ActionUpdate: actDef(fieldOwner|fieldOrg|fieldACL, "edit workspace settings (scheduling, permissions, parameters)"),
90+
ActionDelete: actDef(fieldOwner|fieldOrg|fieldACL, "delete workspace"),
91+
92+
// Workspace provisioning
93+
ActionWorkspaceBuild: actDef(fieldOwner|fieldOrg|fieldACL, "allows starting, stopping, and updating a workspace"),
94+
95+
// Running a workspace
96+
ActionSSH: actDef(fieldOwner|fieldOrg|fieldACL, "ssh into a given workspace"),
97+
ActionApplicationConnect: actDef(fieldOwner|fieldOrg|fieldACL, "connect to workspace apps via browser"),
98+
}
99+
85100
// RBACPermissions is indexed by the type
86101
var RBACPermissions = map[string]PermissionDefinition{
87102
// Wildcard is every object, and the action "*" provides all actions.
@@ -104,23 +119,11 @@ var RBACPermissions = map[string]PermissionDefinition{
104119
},
105120
},
106121
"workspace": {
107-
Actions: map[Action]ActionDefinition{
108-
ActionCreate: actDef(fieldOwner|fieldOrg, "create a new workspace"),
109-
ActionRead: actDef(fieldOwner|fieldOrg|fieldACL, "read workspace data to view on the UI"),
110-
// TODO: Make updates more granular
111-
ActionUpdate: actDef(fieldOwner|fieldOrg|fieldACL, "edit workspace settings (scheduling, permissions, parameters)"),
112-
ActionDelete: actDef(fieldOwner|fieldOrg|fieldACL, "delete workspace"),
113-
114-
// Workspace provisioning
115-
ActionWorkspaceBuild: actDef(fieldOwner|fieldOrg|fieldACL, "allows starting, stopping, and updating a workspace"),
116-
117-
// Running a workspace
118-
ActionSSH: actDef(fieldOwner|fieldOrg|fieldACL, "ssh into a given workspace"),
119-
ActionApplicationConnect: actDef(fieldOwner|fieldOrg|fieldACL, "connect to workspace apps via browser"),
120-
},
122+
Actions: workspaceActions,
121123
},
124+
// Dormant workspaces have the same perms as workspaces.
122125
"workspace_dormant": {
123-
Actions: map[Action]ActionDefinition{},
126+
Actions: workspaceActions,
124127
},
125128
"workspace_proxy": {
126129
Actions: map[Action]ActionDefinition{
@@ -194,8 +197,9 @@ var RBACPermissions = map[string]PermissionDefinition{
194197
"organization": {
195198
Actions: map[Action]ActionDefinition{
196199
ActionCreate: actDef(0, "create an organization"),
197-
ActionRead: actDef(0, "read organizations"),
198-
ActionDelete: actDef(0, "delete a organization"),
200+
ActionRead: actDef(fieldOrg, "read organizations"),
201+
ActionUpdate: actDef(fieldOrg, "update an organization"),
202+
ActionDelete: actDef(fieldOrg, "delete an organization"),
199203
},
200204
},
201205
"organization_member": {

coderd/rbac/roles.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
145145
allPermsExcept(ResourceWorkspaceDormant, ResourceWorkspace),
146146
// This adds back in the Workspace permissions.
147147
Permissions(map[string][]policy.Action{
148-
ResourceWorkspace.Type: ownerWorkspaceActions,
148+
ResourceWorkspace.Type: ownerWorkspaceActions,
149+
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate},
149150
})...),
150151
Org: map[string][]Permission{},
151152
User: []Permission{},
@@ -165,6 +166,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
165166
Org: map[string][]Permission{},
166167
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember),
167168
Permissions(map[string][]policy.Action{
169+
// Reduced permission set on dormant workspaces. No build, ssh, or exec
170+
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate},
171+
168172
// Users cannot do create/update/delete on themselves, but they
169173
// can read their own details.
170174
ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
@@ -268,7 +272,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
268272
Org: map[string][]Permission{
269273
// Org admins should not have workspace exec perms.
270274
organizationID: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant), Permissions(map[string][]policy.Action{
271-
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
275+
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate},
276+
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
272277
})...),
273278
},
274279
User: []Permission{},

coderd/rbac/roles_test.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func TestRolePermissions(t *testing.T) {
265265
},
266266
{
267267
Name: "APIKey",
268-
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
268+
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
269269
Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()),
270270
AuthorizeMap: map[bool][]authSubject{
271271
true: {owner, orgMemberMe, memberMe},
@@ -332,7 +332,16 @@ func TestRolePermissions(t *testing.T) {
332332
},
333333
{
334334
Name: "WorkspaceDormant",
335-
Actions: rbac.AllActions(),
335+
Actions: crud,
336+
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
337+
AuthorizeMap: map[bool][]authSubject{
338+
true: {orgMemberMe, orgAdmin, owner},
339+
false: {userAdmin, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin},
340+
},
341+
},
342+
{
343+
Name: "WorkspaceDormantUse",
344+
Actions: []policy.Action{policy.ActionWorkspaceBuild, policy.ActionApplicationConnect, policy.ActionSSH},
336345
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
337346
AuthorizeMap: map[bool][]authSubject{
338347
true: {},
@@ -478,7 +487,7 @@ func TestRolePermissions(t *testing.T) {
478487
},
479488
{
480489
Name: "Oauth2Token",
481-
Actions: crud,
490+
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
482491
Resource: rbac.ResourceOauth2AppCodeToken,
483492
AuthorizeMap: map[bool][]authSubject{
484493
true: {owner},
@@ -514,6 +523,7 @@ func TestRolePermissions(t *testing.T) {
514523
}
515524
}
516525

526+
passed := true
517527
for _, c := range testCases {
518528
c := c
519529
// nolint:tparallel -- These share the same remainingPermissions map
@@ -524,6 +534,13 @@ func TestRolePermissions(t *testing.T) {
524534
}
525535

526536
for _, action := range c.Actions {
537+
err := c.Resource.ValidAction(action)
538+
ok := assert.NoError(t, err, "%q is not a valid action for type %q", action, c.Resource.Type)
539+
if !ok {
540+
passed = passed && assert.NoError(t, err, "%q is not a valid action for type %q", action, c.Resource.Type)
541+
continue
542+
}
543+
527544
for result, subjs := range c.AuthorizeMap {
528545
for _, subj := range subjs {
529546
delete(remainingSubjs, subj.Name)
@@ -538,9 +555,9 @@ func TestRolePermissions(t *testing.T) {
538555
delete(remainingPermissions[c.Resource.Type], action)
539556
err := auth.Authorize(context.Background(), actor, action, c.Resource)
540557
if result {
541-
assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
558+
passed = passed && assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
542559
} else {
543-
assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg))
560+
passed = passed && assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg))
544561
}
545562
}
546563
}
@@ -549,13 +566,16 @@ func TestRolePermissions(t *testing.T) {
549566
})
550567
}
551568

552-
for rtype, v := range remainingPermissions {
553-
// nolint:tparallel -- Making a subtest for easier diagnosing failures.
554-
t.Run(fmt.Sprintf("%s-AllActions", rtype), func(t *testing.T) {
555-
if len(v) > 0 {
556-
assert.Equal(t, map[policy.Action]bool{}, v, "remaining permissions should be empty for type %q", rtype)
557-
}
558-
})
569+
// Only run these if the tests on top passed. Otherwise, the error output is too noisy.
570+
if passed {
571+
for rtype, v := range remainingPermissions {
572+
// nolint:tparallel -- Making a subtest for easier diagnosing failures.
573+
t.Run(fmt.Sprintf("%s-AllActions", rtype), func(t *testing.T) {
574+
if len(v) > 0 {
575+
assert.Equal(t, map[policy.Action]bool{}, v, "remaining permissions should be empty for type %q", rtype)
576+
}
577+
})
578+
}
559579
}
560580
}
561581

0 commit comments

Comments
 (0)