diff --git a/coderd/authorize_test.go b/coderd/authorize_test.go index f720f90c09206..3af6cfd7d620e 100644 --- a/coderd/authorize_test.go +++ b/coderd/authorize_test.go @@ -103,7 +103,7 @@ func TestCheckPermissions(t *testing.T) { Client: orgAdminClient, UserID: orgAdminUser.ID, Check: map[string]bool{ - readAllUsers: false, + readAllUsers: true, readMyself: true, readOwnWorkspaces: true, readOrgWorkspaces: true, diff --git a/coderd/members_test.go b/coderd/members_test.go index 3066e15a8f783..af8196e6da3cb 100644 --- a/coderd/members_test.go +++ b/coderd/members_test.go @@ -33,20 +33,23 @@ func TestAddMember(t *testing.T) { // Make a user not in the second organization _, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID) - members, err := owner.OrganizationMembers(ctx, org.ID) + // Use scoped user admin in org to add the user + client, userAdmin := coderdtest.CreateAnotherUser(t, owner, org.ID, rbac.ScopedRoleOrgUserAdmin(org.ID)) + + members, err := client.OrganizationMembers(ctx, org.ID) require.NoError(t, err) - require.Len(t, members, 1) // Verify just the 1 member + require.Len(t, members, 2) // Verify the 2 members at the start // Add user to org - _, err = owner.PostOrganizationMember(ctx, org.ID, user.Username) + _, err = client.PostOrganizationMember(ctx, org.ID, user.Username) require.NoError(t, err) - members, err = owner.OrganizationMembers(ctx, org.ID) + members, err = client.OrganizationMembers(ctx, org.ID) require.NoError(t, err) - // Owner + new member - require.Len(t, members, 2) + // Owner + user admin + new member + require.Len(t, members, 3) require.ElementsMatch(t, - []uuid.UUID{first.UserID, user.ID}, + []uuid.UUID{first.UserID, user.ID, userAdmin.ID}, db2sdk.List(members, onlyIDs)) }) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 4804cdce2eae1..4511111feded6 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -27,8 +27,11 @@ const ( customSiteRole string = "custom-site-role" customOrganizationRole string = "custom-organization-role" - orgAdmin string = "organization-admin" - orgMember string = "organization-member" + orgAdmin string = "organization-admin" + orgMember string = "organization-member" + orgAuditor string = "organization-auditor" + orgUserAdmin string = "organization-user-admin" + orgTemplateAdmin string = "organization-template-admin" ) func init() { @@ -144,18 +147,38 @@ func RoleOrgMember() string { return orgMember } +func RoleOrgAuditor() string { + return orgAuditor +} + +func RoleOrgUserAdmin() string { + return orgUserAdmin +} + +func RoleOrgTemplateAdmin() string { + return orgTemplateAdmin +} + // ScopedRoleOrgAdmin is the org role with the organization ID -// Deprecated This was used before organization scope was included as a -// field in all user facing APIs. Usage of 'ScopedRoleOrgAdmin()' is preferred. func ScopedRoleOrgAdmin(organizationID uuid.UUID) RoleIdentifier { - return RoleIdentifier{Name: orgAdmin, OrganizationID: organizationID} + return RoleIdentifier{Name: RoleOrgAdmin(), OrganizationID: organizationID} } // ScopedRoleOrgMember is the org role with the organization ID -// Deprecated This was used before organization scope was included as a -// field in all user facing APIs. Usage of 'ScopedRoleOrgMember()' is preferred. func ScopedRoleOrgMember(organizationID uuid.UUID) RoleIdentifier { - return RoleIdentifier{Name: orgMember, OrganizationID: organizationID} + return RoleIdentifier{Name: RoleOrgMember(), OrganizationID: organizationID} +} + +func ScopedRoleOrgAuditor(organizationID uuid.UUID) RoleIdentifier { + return RoleIdentifier{Name: RoleOrgAuditor(), OrganizationID: organizationID} +} + +func ScopedRoleOrgUserAdmin(organizationID uuid.UUID) RoleIdentifier { + return RoleIdentifier{Name: RoleOrgUserAdmin(), OrganizationID: organizationID} +} + +func ScopedRoleOrgTemplateAdmin(organizationID uuid.UUID) RoleIdentifier { + return RoleIdentifier{Name: RoleOrgTemplateAdmin(), OrganizationID: organizationID} } func allPermsExcept(excepts ...Objecter) []Permission { @@ -365,7 +388,11 @@ func ReloadBuiltinRoles(opts *RoleOptions) { return Role{ Identifier: RoleIdentifier{Name: orgAdmin, OrganizationID: organizationID}, DisplayName: "Organization Admin", - Site: []Permission{}, + Site: Permissions(map[string][]policy.Action{ + // To assign organization members, we need to be able to read + // users at the site wide to know they exist. + ResourceUser.Type: {policy.ActionRead}, + }), Org: map[string][]Permission{ // Org admins should not have workspace exec perms. organizationID.String(): append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourceAssignRole), Permissions(map[string][]policy.Action{ @@ -377,8 +404,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) { } }, - // orgMember has an empty set of permissions, this just implies their membership - // in an organization. + // orgMember is an implied role to any member in an organization. orgMember: func(organizationID uuid.UUID) Role { return Role{ Identifier: RoleIdentifier{Name: orgMember, OrganizationID: organizationID}, @@ -406,6 +432,59 @@ func ReloadBuiltinRoles(opts *RoleOptions) { }, } }, + orgAuditor: func(organizationID uuid.UUID) Role { + return Role{ + Identifier: RoleIdentifier{Name: orgAuditor, OrganizationID: organizationID}, + DisplayName: "Organization Auditor", + Site: []Permission{}, + Org: map[string][]Permission{ + organizationID.String(): Permissions(map[string][]policy.Action{ + ResourceAuditLog.Type: {policy.ActionRead}, + }), + }, + User: []Permission{}, + } + }, + orgUserAdmin: func(organizationID uuid.UUID) Role { + // Manages organization members and groups. + return Role{ + Identifier: RoleIdentifier{Name: orgUserAdmin, OrganizationID: organizationID}, + DisplayName: "Organization User Admin", + Site: Permissions(map[string][]policy.Action{ + // To assign organization members, we need to be able to read + // users at the site wide to know they exist. + ResourceUser.Type: {policy.ActionRead}, + }), + Org: map[string][]Permission{ + organizationID.String(): Permissions(map[string][]policy.Action{ + // Assign, remove, and read roles in the organization. + ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead}, + ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, + ResourceGroup.Type: ResourceGroup.AvailableActions(), + }), + }, + User: []Permission{}, + } + }, + orgTemplateAdmin: func(organizationID uuid.UUID) Role { + // Manages organization members and groups. + return Role{ + Identifier: RoleIdentifier{Name: orgTemplateAdmin, OrganizationID: organizationID}, + DisplayName: "Organization Template Admin", + Site: []Permission{}, + Org: map[string][]Permission{ + organizationID.String(): Permissions(map[string][]policy.Action{ + ResourceTemplate.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights}, + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + ResourceWorkspace.Type: {policy.ActionRead}, + // Assigning template perms requires this permission. + ResourceOrganizationMember.Type: {policy.ActionRead}, + ResourceGroup.Type: {policy.ActionRead}, + }), + }, + User: []Permission{}, + } + }, } } @@ -421,6 +500,9 @@ var assignRoles = map[string]map[string]bool{ member: true, orgAdmin: true, orgMember: true, + orgAuditor: true, + orgUserAdmin: true, + orgTemplateAdmin: true, templateAdmin: true, userAdmin: true, customSiteRole: true, @@ -432,6 +514,9 @@ var assignRoles = map[string]map[string]bool{ member: true, orgAdmin: true, orgMember: true, + orgAuditor: true, + orgUserAdmin: true, + orgTemplateAdmin: true, templateAdmin: true, userAdmin: true, customSiteRole: true, @@ -444,8 +529,14 @@ var assignRoles = map[string]map[string]bool{ orgAdmin: { orgAdmin: true, orgMember: true, + orgAuditor: true, + orgUserAdmin: true, + orgTemplateAdmin: true, customOrganizationRole: true, }, + orgUserAdmin: { + orgMember: true, + }, } // ExpandableRoles is any type that can be expanded into a []Role. This is implemented diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index cedb3d8e1af79..81dacafbf78da 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -14,12 +14,22 @@ import ( "github.com/coder/coder/v2/coderd/rbac/policy" ) +type hasAuthSubjects interface { + Subjects() []authSubject +} + +type authSubjectSet []authSubject + +func (a authSubjectSet) Subjects() []authSubject { return a } + type authSubject struct { // Name is helpful for test assertions Name string Actor rbac.Subject } +func (a authSubject) Subjects() []authSubject { return []authSubject{a} } + // TestBuiltInRoles makes sure our built-in roles are valid by our own policy // rules. If this is incorrect, that is a mistake. func TestBuiltInRoles(t *testing.T) { @@ -89,6 +99,8 @@ func TestRolePermissions(t *testing.T) { currentUser := uuid.New() adminID := uuid.New() templateAdminID := uuid.New() + userAdminID := uuid.New() + auditorID := uuid.New() orgID := uuid.New() otherOrg := uuid.New() workspaceID := uuid.New() @@ -102,17 +114,30 @@ func TestRolePermissions(t *testing.T) { orgMemberMe := authSubject{Name: "org_member_me", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID)}}} owner := authSubject{Name: "owner", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleOwner()}}} + templateAdmin := authSubject{Name: "template-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}} + userAdmin := authSubject{Name: "user-admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleUserAdmin()}}} + orgAdmin := authSubject{Name: "org_admin", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAdmin(orgID)}}} + orgAuditor := authSubject{Name: "org_auditor", Actor: rbac.Subject{ID: auditorID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgAuditor(orgID)}}} + orgUserAdmin := authSubject{Name: "org_user_admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgUserAdmin(orgID)}}} + orgTemplateAdmin := authSubject{Name: "org_template_admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(orgID), rbac.ScopedRoleOrgTemplateAdmin(orgID)}}} + setOrgNotMe := authSubjectSet{orgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin} otherOrgMember := authSubject{Name: "org_member_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg)}}} otherOrgAdmin := authSubject{Name: "org_admin_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg), rbac.ScopedRoleOrgAdmin(otherOrg)}}} - - templateAdmin := authSubject{Name: "template-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}} - userAdmin := authSubject{Name: "user-admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleUserAdmin()}}} + otherOrgAuditor := authSubject{Name: "org_auditor_other", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg), rbac.ScopedRoleOrgAuditor(otherOrg)}}} + otherOrgUserAdmin := authSubject{Name: "org_user_admin_other", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg), rbac.ScopedRoleOrgUserAdmin(otherOrg)}}} + otherOrgTemplateAdmin := authSubject{Name: "org_template_admin_other", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgMember(otherOrg), rbac.ScopedRoleOrgTemplateAdmin(otherOrg)}}} + setOtherOrg := authSubjectSet{otherOrgMember, otherOrgAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin} // requiredSubjects are required to be asserted in each test case. This is // to make sure one is not forgotten. - requiredSubjects := []authSubject{memberMe, owner, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin} + requiredSubjects := []authSubject{ + memberMe, owner, + orgMemberMe, orgAdmin, + otherOrgAdmin, otherOrgMember, orgAuditor, orgUserAdmin, orgTemplateAdmin, + templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, + } testCases := []struct { // Name the test case to better locate the failing test case. @@ -125,24 +150,27 @@ func TestRolePermissions(t *testing.T) { // "false". // true: Subjects who Authorize should return no error // false: Subjects who Authorize should return forbidden. - AuthorizeMap map[bool][]authSubject + AuthorizeMap map[bool][]hasAuthSubjects }{ { Name: "MyUser", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceUserObject(currentUser), - AuthorizeMap: map[bool][]authSubject{ - true: {orgMemberMe, owner, memberMe, templateAdmin, userAdmin}, - false: {otherOrgMember, otherOrgAdmin, orgAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {orgMemberMe, owner, memberMe, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin}, + false: { + orgTemplateAdmin, orgAuditor, + otherOrgMember, otherOrgAuditor, otherOrgTemplateAdmin, + }, }, }, { Name: "AUser", Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceUser, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, - false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin}, }, }, { @@ -150,9 +178,9 @@ func TestRolePermissions(t *testing.T) { // When creating the WithID won't be set, but it does not change the result. Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgMemberMe, orgAdmin, templateAdmin}, - false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgMemberMe, orgAdmin, templateAdmin, orgTemplateAdmin}, + false: {setOtherOrg, memberMe, userAdmin, orgAuditor, orgUserAdmin}, }, }, { @@ -160,9 +188,9 @@ func TestRolePermissions(t *testing.T) { // When creating the WithID won't be set, but it does not change the result. Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgMemberMe, orgAdmin}, - false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin, templateAdmin}, + false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -170,9 +198,9 @@ func TestRolePermissions(t *testing.T) { // When creating the WithID won't be set, but it does not change the result. Actions: []policy.Action{policy.ActionSSH}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgMemberMe}, - false: {orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, }, }, { @@ -180,98 +208,100 @@ func TestRolePermissions(t *testing.T) { // When creating the WithID won't be set, but it does not change the result. Actions: []policy.Action{policy.ActionApplicationConnect}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgMemberMe}, - false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin, orgAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin}, }, }, { Name: "Templates", Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights}, Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, templateAdmin}, - false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember, userAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, orgMemberMe, userAdmin}, }, }, { Name: "ReadTemplates", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceTemplate.InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, templateAdmin}, - false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin, orgMemberMe}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin, orgMemberMe}, }, }, { Name: "Files", Actions: []policy.Action{policy.ActionCreate}, Resource: rbac.ResourceFile.WithID(fileID), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, templateAdmin}, - false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, templateAdmin}, + // Org template admins can only read org scoped files. + // File scope is currently not org scoped :cry: + false: {setOtherOrg, orgTemplateAdmin, orgMemberMe, orgAdmin, memberMe, userAdmin, orgAuditor, orgUserAdmin}, }, }, { Name: "MyFile", Actions: []policy.Action{policy.ActionCreate, policy.ActionRead}, Resource: rbac.ResourceFile.WithID(fileID).WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, memberMe, orgMemberMe, templateAdmin}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, userAdmin}, + false: {setOtherOrg, setOrgNotMe, userAdmin}, }, }, { Name: "CreateOrganizations", Actions: []policy.Action{policy.ActionCreate}, Resource: rbac.ResourceOrganization, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "Organizations", Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "ReadOrganizations", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, orgMemberMe, templateAdmin}, - false: {otherOrgAdmin, otherOrgMember, memberMe, userAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, orgMemberMe, templateAdmin, orgTemplateAdmin, orgAuditor, orgUserAdmin}, + false: {setOtherOrg, memberMe, userAdmin}, }, }, { Name: "CreateCustomRole", Actions: []policy.Action{policy.ActionCreate}, Resource: rbac.ResourceAssignRole, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {userAdmin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, userAdmin, orgMemberMe, memberMe, templateAdmin}, }, }, { Name: "RoleAssignment", Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete}, Resource: rbac.ResourceAssignRole, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, - false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, orgMemberMe, memberMe, templateAdmin}, }, }, { Name: "ReadRoleAssignment", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceAssignRole, - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {setOtherOrg, setOrgNotMe, owner, orgMemberMe, memberMe, templateAdmin, userAdmin}, false: {}, }, }, @@ -279,63 +309,63 @@ func TestRolePermissions(t *testing.T) { Name: "OrgRoleAssignment", Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete}, Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, userAdmin}, - false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, userAdmin, orgUserAdmin}, + false: {setOtherOrg, orgMemberMe, memberMe, templateAdmin, orgTemplateAdmin, orgAuditor}, }, }, { Name: "CreateOrgRoleAssignment", Actions: []policy.Action{policy.ActionCreate}, Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, orgMemberMe, memberMe, templateAdmin, userAdmin}, }, }, { Name: "ReadOrgRoleAssignment", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, orgMemberMe, userAdmin, userAdmin}, - false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, setOrgNotMe, orgMemberMe, userAdmin}, + false: {setOtherOrg, memberMe, templateAdmin}, }, }, { Name: "APIKey", Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete, policy.ActionUpdate}, Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgMemberMe, memberMe}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, templateAdmin, userAdmin}, }, }, { Name: "UserData", Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal}, Resource: rbac.ResourceUserObject(currentUser), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgMemberMe, memberMe, userAdmin}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, templateAdmin}, }, }, { Name: "ManageOrgMember", Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, userAdmin}, - false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, userAdmin, orgUserAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgAuditor, orgMemberMe, memberMe, templateAdmin}, }, }, { Name: "ReadOrgMember", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, userAdmin, orgMemberMe, templateAdmin}, - false: {memberMe, otherOrgAdmin, otherOrgMember}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, userAdmin, orgMemberMe, templateAdmin, orgUserAdmin, orgTemplateAdmin}, + false: {memberMe, setOtherOrg, orgAuditor}, }, }, { @@ -346,54 +376,54 @@ func TestRolePermissions(t *testing.T) { orgID.String(): {policy.ActionRead}, }), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, orgMemberMe, templateAdmin}, - false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, orgMemberMe, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, userAdmin}, }, }, { Name: "Groups", Actions: []policy.Action{policy.ActionCreate, policy.ActionDelete, policy.ActionUpdate}, Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, userAdmin}, - false: {memberMe, otherOrgAdmin, orgMemberMe, otherOrgMember, templateAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, userAdmin, orgUserAdmin}, + false: {setOtherOrg, memberMe, orgMemberMe, templateAdmin, orgTemplateAdmin, orgAuditor}, }, }, { Name: "GroupsRead", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, userAdmin, templateAdmin}, - false: {memberMe, otherOrgAdmin, orgMemberMe, otherOrgMember}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin}, + false: {setOtherOrg, memberMe, orgMemberMe, orgAuditor}, }, }, { Name: "WorkspaceDormant", Actions: append(crud, policy.ActionWorkspaceStop), Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {orgMemberMe, orgAdmin, owner}, - false: {userAdmin, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, + false: {setOtherOrg, userAdmin, memberMe, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { Name: "WorkspaceDormantUse", Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionApplicationConnect, policy.ActionSSH}, Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {}, - false: {memberMe, orgAdmin, userAdmin, otherOrgAdmin, otherOrgMember, orgMemberMe, owner, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, userAdmin, orgMemberMe, owner, templateAdmin}, }, }, { Name: "WorkspaceBuild", Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionWorkspaceStop}, Resource: rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgMemberMe}, - false: {userAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, memberMe}, + false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, // Some admin style resources @@ -401,81 +431,81 @@ func TestRolePermissions(t *testing.T) { Name: "Licenses", Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceLicense, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "DeploymentStats", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceDeploymentStats, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "DeploymentConfig", Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate}, Resource: rbac.ResourceDeploymentConfig, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "DebugInfo", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceDebugInfo, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "Replicas", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceReplicas, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "TailnetCoordinator", Actions: crud, Resource: rbac.ResourceTailnetCoordinator, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "AuditLogs", Actions: []policy.Action{policy.ActionRead, policy.ActionCreate}, Resource: rbac.ResourceAuditLog, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "ProvisionerDaemons", Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgAdmin}, - false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, userAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, memberMe, orgMemberMe, userAdmin, orgAuditor}, }, }, { Name: "ProvisionerDaemonsRead", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ // This should be fixed when multi-org goes live - true: {owner, templateAdmin, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, userAdmin}, + true: {setOtherOrg, owner, templateAdmin, setOrgNotMe, memberMe, orgMemberMe, userAdmin}, false: {}, }, }, @@ -483,44 +513,44 @@ func TestRolePermissions(t *testing.T) { Name: "UserProvisionerDaemons", Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgMemberMe, orgAdmin}, - false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, + false: {setOtherOrg, memberMe, userAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { Name: "ProvisionerKeys", Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceProvisionerKeys.InOrg(orgID), - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, userAdmin, templateAdmin}, + false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, { Name: "System", Actions: crud, Resource: rbac.ResourceSystem, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "Oauth2App", Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceOauth2App, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "Oauth2AppRead", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOauth2App, - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, setOrgNotMe, setOtherOrg, memberMe, orgMemberMe, templateAdmin, userAdmin}, false: {}, }, }, @@ -528,35 +558,35 @@ func TestRolePermissions(t *testing.T) { Name: "Oauth2AppSecret", Actions: crud, Resource: rbac.ResourceOauth2AppSecret, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "Oauth2Token", Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceOauth2AppCodeToken, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "WorkspaceProxy", Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceWorkspaceProxy, - AuthorizeMap: map[bool][]authSubject{ + AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, { Name: "WorkspaceProxyRead", Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceWorkspaceProxy, - AuthorizeMap: map[bool][]authSubject{ - true: {owner, orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, setOrgNotMe, setOtherOrg, memberMe, orgMemberMe, templateAdmin, userAdmin}, false: {}, }, }, @@ -590,8 +620,19 @@ func TestRolePermissions(t *testing.T) { continue } - for result, subjs := range c.AuthorizeMap { + for result, sets := range c.AuthorizeMap { + subjs := make([]authSubject, 0) + for _, set := range sets { + subjs = append(subjs, set.Subjects()...) + } + used := make(map[string]bool) + for _, subj := range subjs { + if _, ok := used[subj.Name]; ok { + assert.False(t, true, "duplicate subject %q", subj.Name) + } + used[subj.Name] = true + delete(remainingSubjs, subj.Name) msg := fmt.Sprintf("%s as %q doing %q on %q", c.Name, subj.Name, action, c.Resource.Type) // TODO: scopey @@ -704,6 +745,9 @@ func TestListRoles(t *testing.T) { require.ElementsMatch(t, []string{ fmt.Sprintf("organization-admin:%s", orgID.String()), fmt.Sprintf("organization-member:%s", orgID.String()), + fmt.Sprintf("organization-auditor:%s", orgID.String()), + fmt.Sprintf("organization-user-admin:%s", orgID.String()), + fmt.Sprintf("organization-template-admin:%s", orgID.String()), }, orgRoleNames) } diff --git a/coderd/roles_test.go b/coderd/roles_test.go index de9724b4bcb4b..9453f610c69bd 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -64,7 +64,10 @@ func TestListRoles(t *testing.T) { return member.ListOrganizationRoles(ctx, owner.OrganizationID) }, ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: false, }), }, { @@ -93,7 +96,10 @@ func TestListRoles(t *testing.T) { return orgAdmin.ListOrganizationRoles(ctx, owner.OrganizationID) }, ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, }), }, { @@ -122,7 +128,10 @@ func TestListRoles(t *testing.T) { return client.ListOrganizationRoles(ctx, owner.OrganizationID) }, ExpectedRoles: convertRoles(map[rbac.RoleIdentifier]bool{ - {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationAuditor, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, }), }, } diff --git a/coderd/templates_test.go b/coderd/templates_test.go index b82d043bba84c..d89240d801fab 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -49,10 +49,13 @@ func TestPostTemplateByOrganization(t *testing.T) { t.Run("Create", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) - owner := coderdtest.CreateFirstUser(t, client) + ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) + owner := coderdtest.CreateFirstUser(t, ownerClient) + + // Use org scoped template admin + client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID)) // By default, everyone in the org can read the template. - user, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + user, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) auditor.ResetLogs() version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) @@ -79,14 +82,16 @@ func TestPostTemplateByOrganization(t *testing.T) { t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + ownerClient := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, ownerClient) + client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID)) + + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitLong) - _, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ + _, err := client.CreateTemplate(ctx, owner.OrganizationID, codersdk.CreateTemplateRequest{ Name: template.Name, VersionID: version.ID, }) diff --git a/codersdk/rbacroles.go b/codersdk/rbacroles.go index fe90d98f77384..49ed5c5b73176 100644 --- a/codersdk/rbacroles.go +++ b/codersdk/rbacroles.go @@ -8,6 +8,9 @@ const ( RoleUserAdmin string = "user-admin" RoleAuditor string = "auditor" - RoleOrganizationAdmin string = "organization-admin" - RoleOrganizationMember string = "organization-member" + RoleOrganizationAdmin string = "organization-admin" + RoleOrganizationMember string = "organization-member" + RoleOrganizationAuditor string = "organization-auditor" + RoleOrganizationTemplateAdmin string = "organization-template-admin" + RoleOrganizationUserAdmin string = "organization-user-admin" )