Skip to content

Commit 40e68cb

Browse files
Emyrkmafredri
andauthored
feat: Add template-admin + user-admin role for managing templates + users (#3490)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
1 parent c41261c commit 40e68cb

File tree

16 files changed

+219
-59
lines changed

16 files changed

+219
-59
lines changed

coderd/coderd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ func New(options *Options) *API {
340340
r.Get("/", api.workspaceAgent)
341341
r.Post("/peer", api.postWorkspaceAgentWireguardPeer)
342342
r.Get("/dial", api.workspaceAgentDial)
343-
r.Get("/turn", api.workspaceAgentTurn)
343+
r.Get("/turn", api.userWorkspaceAgentTurn)
344344
r.Get("/pty", api.workspaceAgentPTY)
345345
r.Get("/iceservers", api.workspaceAgentICEServers)
346346
r.Get("/derp", api.derpMap)

coderd/coderd_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
220220

221221
// Some quick reused objects
222222
workspaceRBACObj := rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(workspace.OwnerID.String())
223+
workspaceExecObj := rbac.ResourceWorkspaceExecution.InOrg(organization.ID).WithOwner(workspace.OwnerID.String())
223224

224225
// skipRoutes allows skipping routes from being checked.
225226
skipRoutes := map[string]string{
@@ -268,7 +269,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
268269
"GET:/api/v2/workspaceagents/me/wireguardlisten": {NoAuthorize: true},
269270
"POST:/api/v2/workspaceagents/me/keys": {NoAuthorize: true},
270271
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
271-
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
272272
"GET:/api/v2/workspaceagents/{workspaceagent}/derp": {NoAuthorize: true},
273273

274274
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
@@ -331,12 +331,16 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
331331
AssertObject: workspaceRBACObj,
332332
},
333333
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {
334-
AssertAction: rbac.ActionUpdate,
335-
AssertObject: workspaceRBACObj,
334+
AssertAction: rbac.ActionCreate,
335+
AssertObject: workspaceExecObj,
336+
},
337+
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {
338+
AssertAction: rbac.ActionCreate,
339+
AssertObject: workspaceExecObj,
336340
},
337341
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {
338-
AssertAction: rbac.ActionUpdate,
339-
AssertObject: workspaceRBACObj,
342+
AssertAction: rbac.ActionCreate,
343+
AssertObject: workspaceExecObj,
340344
},
341345
"GET:/api/v2/workspaces/": {
342346
StatusCode: http.StatusOK,

coderd/database/modelmethods.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ func (w Workspace) RBACObject() rbac.Object {
1717
return rbac.ResourceWorkspace.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
1818
}
1919

20+
func (w Workspace) ExecutionRBAC() rbac.Object {
21+
return rbac.ResourceWorkspaceExecution.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
22+
}
23+
2024
func (m OrganizationMember) RBACObject() rbac.Object {
2125
return rbac.ResourceOrganizationMember.InOrg(m.OrganizationID)
2226
}

coderd/rbac/builtin.go

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
)
1010

1111
const (
12-
admin string = "admin"
13-
member string = "member"
14-
auditor string = "auditor"
12+
admin string = "admin"
13+
member string = "member"
14+
templateAdmin string = "template-admin"
15+
userAdmin string = "user-admin"
16+
auditor string = "auditor"
1517

1618
orgAdmin string = "organization-admin"
1719
orgMember string = "organization-member"
@@ -26,6 +28,14 @@ func RoleAdmin() string {
2628
return roleName(admin, "")
2729
}
2830

31+
func RoleTemplateAdmin() string {
32+
return roleName(templateAdmin, "")
33+
}
34+
35+
func RoleUserAdmin() string {
36+
return roleName(userAdmin, "")
37+
}
38+
2939
func RoleMember() string {
3040
return roleName(member, "")
3141
}
@@ -93,6 +103,31 @@ var (
93103
}
94104
},
95105

106+
templateAdmin: func(_ string) Role {
107+
return Role{
108+
Name: templateAdmin,
109+
DisplayName: "Template Admin",
110+
Site: permissions(map[Object][]Action{
111+
ResourceTemplate: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
112+
// CRUD all files, even those they did not upload.
113+
ResourceFile: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
114+
ResourceWorkspace: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
115+
// CRUD to provisioner daemons for now.
116+
ResourceProvisionerDaemon: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
117+
}),
118+
}
119+
},
120+
121+
userAdmin: func(_ string) Role {
122+
return Role{
123+
Name: userAdmin,
124+
DisplayName: "User Admin",
125+
Site: permissions(map[Object][]Action{
126+
ResourceUser: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
127+
}),
128+
}
129+
},
130+
96131
// orgAdmin returns a role with all actions allows in a given
97132
// organization scope.
98133
orgAdmin: func(organizationID string) Role {
@@ -153,11 +188,13 @@ var (
153188
// map[actor_role][assign_role]<can_assign>
154189
assignRoles = map[string]map[string]bool{
155190
admin: {
156-
admin: true,
157-
auditor: true,
158-
member: true,
159-
orgAdmin: true,
160-
orgMember: true,
191+
admin: true,
192+
auditor: true,
193+
member: true,
194+
orgAdmin: true,
195+
orgMember: true,
196+
templateAdmin: true,
197+
userAdmin: true,
161198
},
162199
orgAdmin: {
163200
orgAdmin: true,

coderd/rbac/builtin_internal_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ func TestRoleByName(t *testing.T) {
1818
}{
1919
{Role: builtInRoles[admin]("")},
2020
{Role: builtInRoles[member]("")},
21+
{Role: builtInRoles[templateAdmin]("")},
22+
{Role: builtInRoles[userAdmin]("")},
2123
{Role: builtInRoles[auditor]("")},
2224

2325
{Role: builtInRoles[orgAdmin](uuid.New().String())},

coderd/rbac/builtin_test.go

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func TestRolePermissions(t *testing.T) {
111111
// currentUser is anything that references "me", "mine", or "my".
112112
currentUser := uuid.New()
113113
adminID := uuid.New()
114+
templateAdminID := uuid.New()
114115
orgID := uuid.New()
115116
otherOrg := uuid.New()
116117

@@ -124,9 +125,12 @@ func TestRolePermissions(t *testing.T) {
124125
otherOrgMember := authSubject{Name: "org_member_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg)}}
125126
otherOrgAdmin := authSubject{Name: "org_admin_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg), rbac.RoleOrgAdmin(otherOrg)}}
126127

128+
templateAdmin := authSubject{Name: "template-admin", UserID: templateAdminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}
129+
userAdmin := authSubject{Name: "user-admin", UserID: templateAdminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleUserAdmin()}}
130+
127131
// requiredSubjects are required to be asserted in each test case. This is
128132
// to make sure one is not forgotten.
129-
requiredSubjects := []authSubject{memberMe, admin, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember}
133+
requiredSubjects := []authSubject{memberMe, admin, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}
130134

131135
testCases := []struct {
132136
// Name the test case to better locate the failing test case.
@@ -146,7 +150,7 @@ func TestRolePermissions(t *testing.T) {
146150
Actions: []rbac.Action{rbac.ActionRead},
147151
Resource: rbac.ResourceUser,
148152
AuthorizeMap: map[bool][]authSubject{
149-
true: {admin, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin},
153+
true: {admin, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin, userAdmin},
150154
false: {},
151155
},
152156
},
@@ -155,8 +159,8 @@ func TestRolePermissions(t *testing.T) {
155159
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
156160
Resource: rbac.ResourceUser,
157161
AuthorizeMap: map[bool][]authSubject{
158-
true: {admin},
159-
false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin},
162+
true: {admin, userAdmin},
163+
false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin},
160164
},
161165
},
162166
{
@@ -165,44 +169,54 @@ func TestRolePermissions(t *testing.T) {
165169
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
166170
Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(currentUser.String()),
167171
AuthorizeMap: map[bool][]authSubject{
168-
true: {admin, orgMemberMe, orgAdmin},
169-
false: {memberMe, otherOrgAdmin, otherOrgMember},
172+
true: {admin, orgMemberMe, orgAdmin, templateAdmin},
173+
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
174+
},
175+
},
176+
{
177+
Name: "MyWorkspaceInOrgExecution",
178+
// When creating the WithID won't be set, but it does not change the result.
179+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
180+
Resource: rbac.ResourceWorkspaceExecution.InOrg(orgID).WithOwner(currentUser.String()),
181+
AuthorizeMap: map[bool][]authSubject{
182+
true: {admin, orgAdmin, orgMemberMe},
183+
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
170184
},
171185
},
172186
{
173187
Name: "Templates",
174188
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
175189
Resource: rbac.ResourceTemplate.InOrg(orgID),
176190
AuthorizeMap: map[bool][]authSubject{
177-
true: {admin, orgAdmin},
178-
false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember},
191+
true: {admin, orgAdmin, templateAdmin},
192+
false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember, userAdmin},
179193
},
180194
},
181195
{
182196
Name: "ReadTemplates",
183197
Actions: []rbac.Action{rbac.ActionRead},
184198
Resource: rbac.ResourceTemplate.InOrg(orgID),
185199
AuthorizeMap: map[bool][]authSubject{
186-
true: {admin, orgMemberMe, orgAdmin},
187-
false: {memberMe, otherOrgAdmin, otherOrgMember},
200+
true: {admin, orgMemberMe, orgAdmin, templateAdmin},
201+
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
188202
},
189203
},
190204
{
191205
Name: "Files",
192206
Actions: []rbac.Action{rbac.ActionCreate},
193207
Resource: rbac.ResourceFile,
194208
AuthorizeMap: map[bool][]authSubject{
195-
true: {admin},
196-
false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember},
209+
true: {admin, templateAdmin},
210+
false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
197211
},
198212
},
199213
{
200214
Name: "MyFile",
201215
Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
202216
Resource: rbac.ResourceFile.WithOwner(currentUser.String()),
203217
AuthorizeMap: map[bool][]authSubject{
204-
true: {admin, memberMe, orgMemberMe},
205-
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
218+
true: {admin, memberMe, orgMemberMe, templateAdmin},
219+
false: {orgAdmin, otherOrgAdmin, otherOrgMember, userAdmin},
206220
},
207221
},
208222
{
@@ -211,7 +225,7 @@ func TestRolePermissions(t *testing.T) {
211225
Resource: rbac.ResourceOrganization,
212226
AuthorizeMap: map[bool][]authSubject{
213227
true: {admin},
214-
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe},
228+
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
215229
},
216230
},
217231
{
@@ -220,7 +234,7 @@ func TestRolePermissions(t *testing.T) {
220234
Resource: rbac.ResourceOrganization.InOrg(orgID),
221235
AuthorizeMap: map[bool][]authSubject{
222236
true: {admin, orgAdmin},
223-
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe},
237+
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
224238
},
225239
},
226240
{
@@ -229,7 +243,7 @@ func TestRolePermissions(t *testing.T) {
229243
Resource: rbac.ResourceOrganization.InOrg(orgID),
230244
AuthorizeMap: map[bool][]authSubject{
231245
true: {admin, orgAdmin, orgMemberMe},
232-
false: {otherOrgAdmin, otherOrgMember, memberMe},
246+
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
233247
},
234248
},
235249
{
@@ -238,15 +252,15 @@ func TestRolePermissions(t *testing.T) {
238252
Resource: rbac.ResourceRoleAssignment,
239253
AuthorizeMap: map[bool][]authSubject{
240254
true: {admin},
241-
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
255+
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
242256
},
243257
},
244258
{
245259
Name: "ReadRoleAssignment",
246260
Actions: []rbac.Action{rbac.ActionRead},
247261
Resource: rbac.ResourceRoleAssignment,
248262
AuthorizeMap: map[bool][]authSubject{
249-
true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
263+
true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
250264
false: {},
251265
},
252266
},
@@ -256,7 +270,7 @@ func TestRolePermissions(t *testing.T) {
256270
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
257271
AuthorizeMap: map[bool][]authSubject{
258272
true: {admin, orgAdmin},
259-
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
273+
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
260274
},
261275
},
262276
{
@@ -265,7 +279,7 @@ func TestRolePermissions(t *testing.T) {
265279
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
266280
AuthorizeMap: map[bool][]authSubject{
267281
true: {admin, orgAdmin, orgMemberMe},
268-
false: {otherOrgAdmin, otherOrgMember, memberMe},
282+
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
269283
},
270284
},
271285
{
@@ -274,7 +288,7 @@ func TestRolePermissions(t *testing.T) {
274288
Resource: rbac.ResourceAPIKey.WithOwner(currentUser.String()),
275289
AuthorizeMap: map[bool][]authSubject{
276290
true: {admin, orgMemberMe, memberMe},
277-
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
291+
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
278292
},
279293
},
280294
{
@@ -283,7 +297,7 @@ func TestRolePermissions(t *testing.T) {
283297
Resource: rbac.ResourceUserData.WithOwner(currentUser.String()),
284298
AuthorizeMap: map[bool][]authSubject{
285299
true: {admin, orgMemberMe, memberMe},
286-
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
300+
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
287301
},
288302
},
289303
{
@@ -292,7 +306,7 @@ func TestRolePermissions(t *testing.T) {
292306
Resource: rbac.ResourceOrganizationMember.InOrg(orgID),
293307
AuthorizeMap: map[bool][]authSubject{
294308
true: {admin, orgAdmin},
295-
false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember},
309+
false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
296310
},
297311
},
298312
{
@@ -301,7 +315,7 @@ func TestRolePermissions(t *testing.T) {
301315
Resource: rbac.ResourceOrganizationMember.InOrg(orgID),
302316
AuthorizeMap: map[bool][]authSubject{
303317
true: {admin, orgAdmin, orgMemberMe},
304-
false: {memberMe, otherOrgAdmin, otherOrgMember},
318+
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
305319
},
306320
},
307321
}
@@ -396,10 +410,14 @@ func TestListRoles(t *testing.T) {
396410

397411
// If this test is ever failing, just update the list to the roles
398412
// expected from the builtin set.
413+
// Always use constant strings, as if the names change, we need to write
414+
// a SQL migration to change the name on the backend.
399415
require.ElementsMatch(t, []string{
400416
"admin",
401417
"member",
402418
"auditor",
419+
"template-admin",
420+
"user-admin",
403421
},
404422
siteRoleNames)
405423

coderd/rbac/object.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ var (
2222
Type: "workspace",
2323
}
2424

25+
// ResourceWorkspaceExecution CRUD. Org + User owner
26+
// create = workspace remote execution
27+
// read = ?
28+
// update = ?
29+
// delete = ?
30+
ResourceWorkspaceExecution = Object{
31+
Type: "workspace_execution",
32+
}
33+
2534
// ResourceAuditLog
2635
// read = access audit log
2736
ResourceAuditLog = Object{

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy