diff --git a/coderd/rbac/builtin.go b/coderd/rbac/builtin.go index 24f45a300d3b1..bfa6ded0a5250 100644 --- a/coderd/rbac/builtin.go +++ b/coderd/rbac/builtin.go @@ -123,7 +123,10 @@ var ( Name: userAdmin, DisplayName: "User Admin", Site: permissions(map[Object][]Action{ - ResourceUser: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceRoleAssignment: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + ResourceUser: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, + // Full perms to manage org members + ResourceOrganizationMember: {ActionCreate, ActionRead, ActionUpdate, ActionDelete}, }), } }, @@ -196,6 +199,10 @@ var ( templateAdmin: true, userAdmin: true, }, + userAdmin: { + member: true, + orgMember: true, + }, orgAdmin: { orgAdmin: true, orgMember: true, diff --git a/coderd/rbac/builtin_test.go b/coderd/rbac/builtin_test.go index 9936c2e1385cb..bbedc1e48527c 100644 --- a/coderd/rbac/builtin_test.go +++ b/coderd/rbac/builtin_test.go @@ -119,7 +119,7 @@ func TestRolePermissions(t *testing.T) { memberMe := authSubject{Name: "member_me", UserID: currentUser.String(), Roles: []string{rbac.RoleMember()}} orgMemberMe := authSubject{Name: "org_member_me", UserID: currentUser.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(orgID)}} - admin := authSubject{Name: "admin", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOwner()}} + owner := authSubject{Name: "owner", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOwner()}} orgAdmin := authSubject{Name: "org_admin", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(orgID), rbac.RoleOrgAdmin(orgID)}} otherOrgMember := authSubject{Name: "org_member_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg)}} @@ -130,7 +130,7 @@ func TestRolePermissions(t *testing.T) { // 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, templateAdmin, userAdmin} + requiredSubjects := []authSubject{memberMe, owner, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin} testCases := []struct { // Name the test case to better locate the failing test case. @@ -150,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, templateAdmin, userAdmin}, + true: {owner, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin, userAdmin}, false: {}, }, }, @@ -159,7 +159,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceUser, AuthorizeMap: map[bool][]authSubject{ - true: {admin, userAdmin}, + true: {owner, userAdmin}, false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin}, }, }, @@ -169,7 +169,7 @@ 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, templateAdmin}, + true: {owner, orgMemberMe, orgAdmin, templateAdmin}, false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, }, }, @@ -179,7 +179,7 @@ func TestRolePermissions(t *testing.T) { 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}, + true: {owner, orgAdmin, orgMemberMe}, false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, }, }, @@ -188,7 +188,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceTemplate.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgAdmin, templateAdmin}, + true: {owner, orgAdmin, templateAdmin}, false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember, userAdmin}, }, }, @@ -197,7 +197,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionRead}, Resource: rbac.ResourceTemplate.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgMemberMe, orgAdmin, templateAdmin}, + true: {owner, orgMemberMe, orgAdmin, templateAdmin}, false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, }, }, @@ -206,7 +206,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate}, Resource: rbac.ResourceFile, AuthorizeMap: map[bool][]authSubject{ - true: {admin, templateAdmin}, + true: {owner, templateAdmin}, false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, userAdmin}, }, }, @@ -215,7 +215,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceFile.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]authSubject{ - true: {admin, memberMe, orgMemberMe, templateAdmin}, + true: {owner, memberMe, orgMemberMe, templateAdmin}, false: {orgAdmin, otherOrgAdmin, otherOrgMember, userAdmin}, }, }, @@ -224,7 +224,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate}, Resource: rbac.ResourceOrganization, AuthorizeMap: map[bool][]authSubject{ - true: {admin}, + true: {owner}, false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, @@ -233,7 +233,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceOrganization.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgAdmin}, + true: {owner, orgAdmin}, false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin}, }, }, @@ -242,7 +242,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionRead}, Resource: rbac.ResourceOrganization.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgAdmin, orgMemberMe}, + true: {owner, orgAdmin, orgMemberMe}, false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, }, }, @@ -251,8 +251,8 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceRoleAssignment, AuthorizeMap: map[bool][]authSubject{ - true: {admin}, - false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, + true: {owner, userAdmin}, + false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin}, }, }, { @@ -260,7 +260,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionRead}, Resource: rbac.ResourceRoleAssignment, AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, + true: {owner, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, false: {}, }, }, @@ -269,7 +269,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgAdmin}, + true: {owner, orgAdmin}, false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, }, }, @@ -278,7 +278,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionRead}, Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgAdmin, orgMemberMe}, + true: {owner, orgAdmin, orgMemberMe}, false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin}, }, }, @@ -287,7 +287,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceAPIKey.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgMemberMe, memberMe}, + true: {owner, orgMemberMe, memberMe}, false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, }, }, @@ -296,7 +296,7 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceUserData.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgMemberMe, memberMe}, + true: {owner, orgMemberMe, memberMe}, false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, }, }, @@ -305,8 +305,8 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete}, Resource: rbac.ResourceOrganizationMember.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgAdmin}, - false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, + true: {owner, orgAdmin, userAdmin}, + false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin}, }, }, { @@ -314,8 +314,8 @@ func TestRolePermissions(t *testing.T) { Actions: []rbac.Action{rbac.ActionRead}, Resource: rbac.ResourceOrganizationMember.InOrg(orgID), AuthorizeMap: map[bool][]authSubject{ - true: {admin, orgAdmin, orgMemberMe}, - false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}, + true: {owner, orgAdmin, orgMemberMe, userAdmin}, + false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin}, }, }, } diff --git a/coderd/roles.go b/coderd/roles.go index c8f3d95e112af..3370d2248b99b 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -20,14 +20,7 @@ func (api *API) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) { } roles := rbac.SiteRoles() - assignable := make([]rbac.Role, 0) - for _, role := range roles { - if rbac.CanAssignRole(actorRoles.Roles, role.Name) { - assignable = append(assignable, role) - } - } - - httpapi.Write(rw, http.StatusOK, convertRoles(assignable)) + httpapi.Write(rw, http.StatusOK, assignableRoles(actorRoles.Roles, roles)) } // assignableSiteRoles returns all site wide roles that can be assigned. @@ -41,14 +34,7 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { } roles := rbac.OrganizationRoles(organization.ID) - assignable := make([]rbac.Role, 0) - for _, role := range roles { - if rbac.CanAssignRole(actorRoles.Roles, role.Name) { - assignable = append(assignable, role) - } - } - - httpapi.Write(rw, http.StatusOK, convertRoles(assignable)) + httpapi.Write(rw, http.StatusOK, assignableRoles(actorRoles.Roles, roles)) } func (api *API) checkPermissions(rw http.ResponseWriter, r *http.Request) { @@ -102,14 +88,19 @@ func convertRole(role rbac.Role) codersdk.Role { } } -func convertRoles(roles []rbac.Role) []codersdk.Role { - converted := make([]codersdk.Role, 0, len(roles)) +func assignableRoles(actorRoles []string, roles []rbac.Role) []codersdk.AssignableRoles { + assignable := make([]codersdk.AssignableRoles, 0) for _, role := range roles { - // Roles without display names should never be shown to the ui. if role.DisplayName == "" { continue } - converted = append(converted, convertRole(role)) + assignable = append(assignable, codersdk.AssignableRoles{ + Role: codersdk.Role{ + Name: role.Name, + DisplayName: role.DisplayName, + }, + Assignable: rbac.CanAssignRole(actorRoles, role.Name), + }) } - return converted + return assignable } diff --git a/coderd/roles_test.go b/coderd/roles_test.go index 7dcc5354d7355..034be6a6bb43a 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -120,35 +120,39 @@ func TestListRoles(t *testing.T) { require.NoError(t, err, "create org") const forbidden = "Forbidden" - siteRoles := convertRoles(rbac.RoleOwner(), "auditor", "template-admin", "user-admin") - orgRoles := convertRoles(rbac.RoleOrgAdmin(admin.OrganizationID)) - testCases := []struct { Name string Client *codersdk.Client - APICall func(context.Context) ([]codersdk.Role, error) - ExpectedRoles []codersdk.Role + APICall func(context.Context) ([]codersdk.AssignableRoles, error) + ExpectedRoles []codersdk.AssignableRoles AuthorizedError string }{ { // Members cannot assign any roles Name: "MemberListSite", - APICall: func(ctx context.Context) ([]codersdk.Role, error) { + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { x, err := member.ListSiteRoles(ctx) return x, err }, - ExpectedRoles: []codersdk.Role{}, + ExpectedRoles: convertRoles(map[string]bool{ + "owner": false, + "auditor": false, + "template-admin": false, + "user-admin": false, + }), }, { Name: "OrgMemberListOrg", - APICall: func(ctx context.Context) ([]codersdk.Role, error) { + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { return member.ListOrganizationRoles(ctx, admin.OrganizationID) }, - ExpectedRoles: []codersdk.Role{}, + ExpectedRoles: convertRoles(map[string]bool{ + rbac.RoleOrgAdmin(admin.OrganizationID): false, + }), }, { Name: "NonOrgMemberListOrg", - APICall: func(ctx context.Context) ([]codersdk.Role, error) { + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { return member.ListOrganizationRoles(ctx, otherOrg.ID) }, AuthorizedError: forbidden, @@ -156,21 +160,28 @@ func TestListRoles(t *testing.T) { // Org admin { Name: "OrgAdminListSite", - APICall: func(ctx context.Context) ([]codersdk.Role, error) { + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { return orgAdmin.ListSiteRoles(ctx) }, - ExpectedRoles: []codersdk.Role{}, + ExpectedRoles: convertRoles(map[string]bool{ + "owner": false, + "auditor": false, + "template-admin": false, + "user-admin": false, + }), }, { Name: "OrgAdminListOrg", - APICall: func(ctx context.Context) ([]codersdk.Role, error) { + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { return orgAdmin.ListOrganizationRoles(ctx, admin.OrganizationID) }, - ExpectedRoles: orgRoles, + ExpectedRoles: convertRoles(map[string]bool{ + rbac.RoleOrgAdmin(admin.OrganizationID): true, + }), }, { Name: "OrgAdminListOtherOrg", - APICall: func(ctx context.Context) ([]codersdk.Role, error) { + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { return orgAdmin.ListOrganizationRoles(ctx, otherOrg.ID) }, AuthorizedError: forbidden, @@ -178,17 +189,24 @@ func TestListRoles(t *testing.T) { // Admin { Name: "AdminListSite", - APICall: func(ctx context.Context) ([]codersdk.Role, error) { + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { return client.ListSiteRoles(ctx) }, - ExpectedRoles: siteRoles, + ExpectedRoles: convertRoles(map[string]bool{ + "owner": true, + "auditor": true, + "template-admin": true, + "user-admin": true, + }), }, { Name: "AdminListOrg", - APICall: func(ctx context.Context) ([]codersdk.Role, error) { + APICall: func(ctx context.Context) ([]codersdk.AssignableRoles, error) { return client.ListOrganizationRoles(ctx, admin.OrganizationID) }, - ExpectedRoles: orgRoles, + ExpectedRoles: convertRoles(map[string]bool{ + rbac.RoleOrgAdmin(admin.OrganizationID): true, + }), }, } @@ -222,10 +240,14 @@ func convertRole(roleName string) codersdk.Role { } } -func convertRoles(roleNames ...string) []codersdk.Role { - converted := make([]codersdk.Role, 0, len(roleNames)) - for _, roleName := range roleNames { - converted = append(converted, convertRole(roleName)) +func convertRoles(assignableRoles map[string]bool) []codersdk.AssignableRoles { + converted := make([]codersdk.AssignableRoles, 0, len(assignableRoles)) + for roleName, assignable := range assignableRoles { + role := convertRole(roleName) + converted = append(converted, codersdk.AssignableRoles{ + Role: role, + Assignable: assignable, + }) } return converted } diff --git a/codersdk/roles.go b/codersdk/roles.go index d6e34c7e48127..34e2800ac5a42 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -14,8 +14,13 @@ type Role struct { DisplayName string `json:"display_name"` } +type AssignableRoles struct { + Role + Assignable bool `json:"assignable"` +} + // ListSiteRoles lists all assignable site wide roles. -func (c *Client) ListSiteRoles(ctx context.Context) ([]Role, error) { +func (c *Client) ListSiteRoles(ctx context.Context) ([]AssignableRoles, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/roles", nil) if err != nil { return nil, err @@ -24,12 +29,12 @@ func (c *Client) ListSiteRoles(ctx context.Context) ([]Role, error) { if res.StatusCode != http.StatusOK { return nil, readBodyAsError(res) } - var roles []Role + var roles []AssignableRoles return roles, json.NewDecoder(res.Body).Decode(&roles) } // ListOrganizationRoles lists all assignable roles for a given organization. -func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]Role, error) { +func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]AssignableRoles, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/roles", org.String()), nil) if err != nil { return nil, err @@ -38,7 +43,7 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]Ro if res.StatusCode != http.StatusOK { return nil, readBodyAsError(res) } - var roles []Role + var roles []AssignableRoles return roles, json.NewDecoder(res.Body).Decode(&roles) } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index fc232faf87d16..5d40a668400f3 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -311,8 +311,8 @@ export const updateUserPassword = async ( updatePassword: TypesGen.UpdateUserPasswordRequest, ): Promise => axios.put(`/api/v2/users/${userId}/password`, updatePassword) -export const getSiteRoles = async (): Promise> => { - const response = await axios.get>(`/api/v2/users/roles`) +export const getSiteRoles = async (): Promise> => { + const response = await axios.get>(`/api/v2/users/roles`) return response.data } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index def9cd07e894a..e849b1a1f051f 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -24,6 +24,11 @@ export interface AgentGitSSHKey { readonly private_key: string } +// From codersdk/roles.go +export interface AssignableRoles extends Role { + readonly assignable: boolean +} + // From codersdk/users.go export interface AuthMethods { readonly password: boolean diff --git a/site/src/components/RoleSelect/RoleSelect.stories.tsx b/site/src/components/RoleSelect/RoleSelect.stories.tsx index ab3949baa63ac..7cb6717873f8d 100644 --- a/site/src/components/RoleSelect/RoleSelect.stories.tsx +++ b/site/src/components/RoleSelect/RoleSelect.stories.tsx @@ -1,5 +1,12 @@ import { ComponentMeta, Story } from "@storybook/react" -import { MockAdminRole, MockMemberRole, MockSiteRoles } from "../../testHelpers/renderHelpers" +import { + assignableRole, + MockAuditorRole, + MockMemberRole, + MockOwnerRole, + MockTemplateAdminRole, + MockUserAdminRole, +} from "../../testHelpers/renderHelpers" import { RoleSelect, RoleSelectProps } from "./RoleSelect" export default { @@ -9,15 +16,30 @@ export default { const Template: Story = (args) => +// Include 4 roles: +// - owner (disabled, not checked) +// - template admin (disabled, checked) +// - auditor (enabled, not checked) +// - user admin (enabled, checked) export const Close = Template.bind({}) Close.args = { - roles: MockSiteRoles, - selectedRoles: [MockAdminRole, MockMemberRole], + roles: [ + assignableRole(MockOwnerRole, false), + assignableRole(MockTemplateAdminRole, false), + assignableRole(MockAuditorRole, true), + assignableRole(MockUserAdminRole, true), + ], + selectedRoles: [MockUserAdminRole, MockTemplateAdminRole, MockMemberRole], } export const Open = Template.bind({}) Open.args = { open: true, - roles: MockSiteRoles, - selectedRoles: [MockAdminRole, MockMemberRole], + roles: [ + assignableRole(MockOwnerRole, false), + assignableRole(MockTemplateAdminRole, false), + assignableRole(MockAuditorRole, true), + assignableRole(MockUserAdminRole, true), + ], + selectedRoles: [MockUserAdminRole, MockTemplateAdminRole, MockMemberRole], } diff --git a/site/src/components/RoleSelect/RoleSelect.test.tsx b/site/src/components/RoleSelect/RoleSelect.test.tsx new file mode 100644 index 0000000000000..51eb4e050701c --- /dev/null +++ b/site/src/components/RoleSelect/RoleSelect.test.tsx @@ -0,0 +1,44 @@ +import { screen } from "@testing-library/react" +import { + assignableRole, + MockAuditorRole, + MockMemberRole, + MockOwnerRole, + MockTemplateAdminRole, + MockUserAdminRole, + render, +} from "testHelpers/renderHelpers" +import { RoleSelect } from "./RoleSelect" + +describe("UserRoleSelect", () => { + it("renders content", async () => { + // When + render( + , + ) + + // Then + const owner = await screen.findByText(MockOwnerRole.display_name) + const templateAdmin = await screen.findByText(MockTemplateAdminRole.display_name) + const auditor = await screen.findByText(MockAuditorRole.display_name) + const userAdmin = await screen.findByText(MockUserAdminRole.display_name) + + // The attributes are "strings", not boolean types. + expect(owner.getAttribute("aria-disabled")).toBe("true") + expect(templateAdmin.getAttribute("aria-disabled")).toBe("true") + + expect(userAdmin.getAttribute("aria-disabled")).toBe("false") + expect(auditor.getAttribute("aria-disabled")).toBe("false") + }) +}) diff --git a/site/src/components/RoleSelect/RoleSelect.tsx b/site/src/components/RoleSelect/RoleSelect.tsx index a0bfb49bb25ee..1a77eb02784ef 100644 --- a/site/src/components/RoleSelect/RoleSelect.tsx +++ b/site/src/components/RoleSelect/RoleSelect.tsx @@ -3,13 +3,13 @@ import MenuItem from "@material-ui/core/MenuItem" import Select from "@material-ui/core/Select" import { makeStyles, Theme } from "@material-ui/core/styles" import { FC } from "react" -import { Role } from "../../api/typesGenerated" +import { AssignableRoles, Role } from "../../api/typesGenerated" export const Language = { label: "Roles", } export interface RoleSelectProps { - roles: Role[] + roles: AssignableRoles[] selectedRoles: Role[] onChange: (roles: Role["name"][]) => void loading?: boolean @@ -46,7 +46,7 @@ export const RoleSelect: FC = ({ const isChecked = selectedRoles.some((selectedRole) => selectedRole.name === r.name) return ( - + {r.display_name} ) diff --git a/site/src/components/UserDropdownContent/UserDropdownContent.test.tsx b/site/src/components/UserDropdownContent/UserDropdownContent.test.tsx index 48b123ebdd06c..652341cd81ad8 100644 --- a/site/src/components/UserDropdownContent/UserDropdownContent.test.tsx +++ b/site/src/components/UserDropdownContent/UserDropdownContent.test.tsx @@ -1,5 +1,5 @@ import { screen } from "@testing-library/react" -import { MockAdminRole, MockUser } from "../../testHelpers/entities" +import { MockOwnerRole, MockUser } from "../../testHelpers/entities" import { render } from "../../testHelpers/renderHelpers" import { Language, UserDropdownContent } from "./UserDropdownContent" @@ -26,7 +26,7 @@ describe("UserDropdownContent", () => { it("displays the user's roles", () => { render() - expect(screen.getByText(MockAdminRole.display_name)).toBeDefined() + expect(screen.getByText(MockOwnerRole.display_name)).toBeDefined() }) it("has the correct link for the account item", () => { diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx index fc49dca3b1b34..8dcb2d8fa8e25 100644 --- a/site/src/components/UsersTable/UsersTable.tsx +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -18,7 +18,7 @@ export const Language = { export interface UsersTableProps { users?: TypesGen.User[] - roles?: TypesGen.Role[] + roles?: TypesGen.AssignableRoles[] isUpdatingUserRoles?: boolean canEditUsers?: boolean isLoading?: boolean diff --git a/site/src/components/UsersTable/UsersTableBody.tsx b/site/src/components/UsersTable/UsersTableBody.tsx index 3e021e6f7c79d..441c13e372fd9 100644 --- a/site/src/components/UsersTable/UsersTableBody.tsx +++ b/site/src/components/UsersTable/UsersTableBody.tsx @@ -20,7 +20,7 @@ export const Language = { interface UsersTableBodyProps { users?: TypesGen.User[] - roles?: TypesGen.Role[] + roles?: TypesGen.AssignableRoles[] isUpdatingUserRoles?: boolean canEditUsers?: boolean isLoading?: boolean diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index 35f681da8fd4d..507b7b48dffe7 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -311,7 +311,7 @@ describe("Users Page", () => { }, MockAuditorRole) // Check if the select text was updated with the Auditor role - await waitFor(() => expect(rolesMenuTrigger).toHaveTextContent("Admin, Auditor")) + await waitFor(() => expect(rolesMenuTrigger).toHaveTextContent("Owner, Auditor")) // Check if the API was called correctly const currentRoles = MockUser.roles.map((r) => r.name) diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index d411282a89177..a11b68bcb4980 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -17,7 +17,7 @@ export const Language = { export interface UsersPageViewProps { users?: TypesGen.User[] - roles?: TypesGen.Role[] + roles?: TypesGen.AssignableRoles[] filter?: string error?: unknown isUpdatingUserRoles?: boolean diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 1e08d88f4eeb8..738c29459ff4a 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -15,9 +15,19 @@ export const MockBuildInfo: TypesGen.BuildInfoResponse = { version: "v99.999.9999+c9cdf14", } -export const MockAdminRole: TypesGen.Role = { - name: "admin", - display_name: "Admin", +export const MockOwnerRole: TypesGen.Role = { + name: "owner", + display_name: "Owner", +} + +export const MockUserAdminRole: TypesGen.Role = { + name: "user_admin", + display_name: "User Admin", +} + +export const MockTemplateAdminRole: TypesGen.Role = { + name: "template_admin", + display_name: "Template Admin", } export const MockMemberRole: TypesGen.Role = { @@ -30,7 +40,16 @@ export const MockAuditorRole: TypesGen.Role = { display_name: "Auditor", } -export const MockSiteRoles = [MockAdminRole, MockAuditorRole] +export const MockSiteRoles = [MockUserAdminRole, MockAuditorRole] + +// assignableRole takes a role and a boolean. The boolean implies if the +// actor can assign (add/remove) the role from other users. +export function assignableRole(role: TypesGen.Role, assignable: boolean): TypesGen.AssignableRoles { + return { + ...role, + assignable: assignable, + } +} export const MockUser: TypesGen.User = { id: "test-user", @@ -39,7 +58,7 @@ export const MockUser: TypesGen.User = { created_at: "", status: "active", organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"], - roles: [MockAdminRole], + roles: [MockOwnerRole], } export const MockUser2: TypesGen.User = { diff --git a/site/src/xServices/roles/siteRolesXService.ts b/site/src/xServices/roles/siteRolesXService.ts index 2380150581fb4..ee9eaa47d8198 100644 --- a/site/src/xServices/roles/siteRolesXService.ts +++ b/site/src/xServices/roles/siteRolesXService.ts @@ -8,7 +8,7 @@ export const Language = { } type SiteRolesContext = { - roles?: TypesGen.Role[] + roles?: TypesGen.AssignableRoles[] getRolesError: Error | unknown } @@ -25,7 +25,7 @@ export const siteRolesMachine = createMachine( events: {} as SiteRolesEvent, services: { getRoles: { - data: {} as TypesGen.Role[], + data: {} as TypesGen.AssignableRoles[], }, }, },