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

16 files changed

+219
-59
lines changed

coderd/coderd.go

+1-1
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

+9-5
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

+4
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

+45-8
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

+2
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

+43-25
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

+9
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)