Skip to content

feat: Add template-admin + user-admin role for managing templates + users #3490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 12, 2022
2 changes: 1 addition & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func New(options *Options) *API {
r.Get("/", api.workspaceAgent)
r.Post("/peer", api.postWorkspaceAgentWireguardPeer)
r.Get("/dial", api.workspaceAgentDial)
r.Get("/turn", api.workspaceAgentTurn)
r.Get("/turn", api.userWorkspaceAgentTurn)
r.Get("/pty", api.workspaceAgentPTY)
r.Get("/iceservers", api.workspaceAgentICEServers)
r.Get("/derp", api.derpMap)
Expand Down
14 changes: 9 additions & 5 deletions coderd/coderd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {

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

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

// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
Expand Down Expand Up @@ -331,12 +331,16 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
AssertObject: workspaceRBACObj,
},
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {
AssertAction: rbac.ActionUpdate,
AssertObject: workspaceRBACObj,
AssertAction: rbac.ActionCreate,
AssertObject: workspaceExecObj,
},
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {
AssertAction: rbac.ActionCreate,
AssertObject: workspaceExecObj,
},
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {
AssertAction: rbac.ActionUpdate,
AssertObject: workspaceRBACObj,
AssertAction: rbac.ActionCreate,
AssertObject: workspaceExecObj,
},
"GET:/api/v2/workspaces/": {
StatusCode: http.StatusOK,
Expand Down
4 changes: 4 additions & 0 deletions coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ func (w Workspace) RBACObject() rbac.Object {
return rbac.ResourceWorkspace.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
}

func (w Workspace) ExecutionRBAC() rbac.Object {
return rbac.ResourceWorkspaceExecution.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
}

func (m OrganizationMember) RBACObject() rbac.Object {
return rbac.ResourceOrganizationMember.InOrg(m.OrganizationID)
}
Expand Down
53 changes: 45 additions & 8 deletions coderd/rbac/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
)

const (
admin string = "admin"
member string = "member"
auditor string = "auditor"
admin string = "admin"
member string = "member"
templateAdmin string = "template-admin"
userAdmin string = "user-admin"
auditor string = "auditor"

orgAdmin string = "organization-admin"
orgMember string = "organization-member"
Expand All @@ -26,6 +28,14 @@ func RoleAdmin() string {
return roleName(admin, "")
}

func RoleTemplateAdmin() string {
return roleName(templateAdmin, "")
}

func RoleUserAdmin() string {
return roleName(userAdmin, "")
}

func RoleMember() string {
return roleName(member, "")
}
Expand Down Expand Up @@ -93,6 +103,31 @@ var (
}
},

templateAdmin: func(_ string) Role {
return Role{
Name: templateAdmin,
DisplayName: "Template Admin",
Site: permissions(map[Object][]Action{
ResourceTemplate: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// CRUD all files, even those they did not upload.
ResourceFile: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceWorkspace: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// CRUD to provisioner daemons for now.
ResourceProvisionerDaemon: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
}),
}
},

userAdmin: func(_ string) Role {
return Role{
Name: userAdmin,
DisplayName: "User Admin",
Site: permissions(map[Object][]Action{
ResourceUser: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
}),
}
},

// orgAdmin returns a role with all actions allows in a given
// organization scope.
orgAdmin: func(organizationID string) Role {
Expand Down Expand Up @@ -153,11 +188,13 @@ var (
// map[actor_role][assign_role]<can_assign>
assignRoles = map[string]map[string]bool{
admin: {
admin: true,
auditor: true,
member: true,
orgAdmin: true,
orgMember: true,
admin: true,
auditor: true,
member: true,
orgAdmin: true,
orgMember: true,
templateAdmin: true,
userAdmin: true,
},
orgAdmin: {
orgAdmin: true,
Expand Down
2 changes: 2 additions & 0 deletions coderd/rbac/builtin_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ func TestRoleByName(t *testing.T) {
}{
{Role: builtInRoles[admin]("")},
{Role: builtInRoles[member]("")},
{Role: builtInRoles[templateAdmin]("")},
{Role: builtInRoles[userAdmin]("")},
{Role: builtInRoles[auditor]("")},

{Role: builtInRoles[orgAdmin](uuid.New().String())},
Expand Down
68 changes: 43 additions & 25 deletions coderd/rbac/builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func TestRolePermissions(t *testing.T) {
// currentUser is anything that references "me", "mine", or "my".
currentUser := uuid.New()
adminID := uuid.New()
templateAdminID := uuid.New()
orgID := uuid.New()
otherOrg := uuid.New()

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

templateAdmin := authSubject{Name: "template-admin", UserID: templateAdminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}
userAdmin := authSubject{Name: "user-admin", UserID: templateAdminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleUserAdmin()}}

// requiredSubjects are required to be asserted in each test case. This is
// to make sure one is not forgotten.
requiredSubjects := []authSubject{memberMe, admin, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember}
requiredSubjects := []authSubject{memberMe, admin, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}

testCases := []struct {
// Name the test case to better locate the failing test case.
Expand All @@ -146,7 +150,7 @@ func TestRolePermissions(t *testing.T) {
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceUser,
AuthorizeMap: map[bool][]authSubject{
true: {admin, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin},
true: {admin, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin, userAdmin},
false: {},
},
},
Expand All @@ -155,8 +159,8 @@ func TestRolePermissions(t *testing.T) {
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceUser,
AuthorizeMap: map[bool][]authSubject{
true: {admin},
false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin},
true: {admin, userAdmin},
false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin},
},
},
{
Expand All @@ -165,44 +169,54 @@ func TestRolePermissions(t *testing.T) {
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgMemberMe, orgAdmin},
false: {memberMe, otherOrgAdmin, otherOrgMember},
true: {admin, orgMemberMe, orgAdmin, templateAdmin},
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
},
},
{
Name: "MyWorkspaceInOrgExecution",
// When creating the WithID won't be set, but it does not change the result.
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceWorkspaceExecution.InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin, orgMemberMe},
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
Comment on lines +177 to +183
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workspace execution can only be done by admin, orgadmin and the owner of the workspace.

},
},
{
Name: "Templates",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceTemplate.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin},
false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember},
true: {admin, orgAdmin, templateAdmin},
false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember, userAdmin},
},
},
{
Name: "ReadTemplates",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceTemplate.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgMemberMe, orgAdmin},
false: {memberMe, otherOrgAdmin, otherOrgMember},
true: {admin, orgMemberMe, orgAdmin, templateAdmin},
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
},
},
{
Name: "Files",
Actions: []rbac.Action{rbac.ActionCreate},
Resource: rbac.ResourceFile,
AuthorizeMap: map[bool][]authSubject{
true: {admin},
false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember},
true: {admin, templateAdmin},
false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
},
},
{
Name: "MyFile",
Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceFile.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {admin, memberMe, orgMemberMe},
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
true: {admin, memberMe, orgMemberMe, templateAdmin},
false: {orgAdmin, otherOrgAdmin, otherOrgMember, userAdmin},
},
},
{
Expand All @@ -211,7 +225,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceOrganization,
AuthorizeMap: map[bool][]authSubject{
true: {admin},
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe},
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
},
},
{
Expand All @@ -220,7 +234,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceOrganization.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin},
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe},
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
},
},
{
Expand All @@ -229,7 +243,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceOrganization.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin, orgMemberMe},
false: {otherOrgAdmin, otherOrgMember, memberMe},
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
},
},
{
Expand All @@ -238,15 +252,15 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceRoleAssignment,
AuthorizeMap: map[bool][]authSubject{
true: {admin},
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
},
},
{
Name: "ReadRoleAssignment",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceRoleAssignment,
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
false: {},
},
},
Expand All @@ -256,7 +270,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin},
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
},
},
{
Expand All @@ -265,7 +279,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin, orgMemberMe},
false: {otherOrgAdmin, otherOrgMember, memberMe},
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
},
},
{
Expand All @@ -274,7 +288,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceAPIKey.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgMemberMe, memberMe},
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
},
},
{
Expand All @@ -283,7 +297,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceUserData.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgMemberMe, memberMe},
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
},
},
{
Expand All @@ -292,7 +306,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceOrganizationMember.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin},
false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember},
false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
},
},
{
Expand All @@ -301,7 +315,7 @@ func TestRolePermissions(t *testing.T) {
Resource: rbac.ResourceOrganizationMember.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {admin, orgAdmin, orgMemberMe},
false: {memberMe, otherOrgAdmin, otherOrgMember},
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
},
},
}
Expand Down Expand Up @@ -396,10 +410,14 @@ func TestListRoles(t *testing.T) {

// If this test is ever failing, just update the list to the roles
// expected from the builtin set.
// Always use constant strings, as if the names change, we need to write
// a SQL migration to change the name on the backend.
require.ElementsMatch(t, []string{
"admin",
"member",
"auditor",
"template-admin",
"user-admin",
},
siteRoleNames)

Expand Down
9 changes: 9 additions & 0 deletions coderd/rbac/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ var (
Type: "workspace",
}

// ResourceWorkspaceExecution CRUD. Org + User owner
// create = workspace remote execution
// read = ?
// update = ?
// delete = ?
ResourceWorkspaceExecution = Object{
Type: "workspace_execution",
}

// ResourceAuditLog
// read = access audit log
ResourceAuditLog = Object{
Expand Down
Loading