fix: Role assign ui fixes (#3521)

Co-authored-by: Kira Pilot <kira@coder.com>
This commit is contained in:
Steven Masley 2022-08-16 10:39:42 -05:00 committed by GitHub
parent 4b6a82f92a
commit 4be61d9250
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 211 additions and 96 deletions

View File

@ -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,

View File

@ -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},
},
},
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -311,8 +311,8 @@ export const updateUserPassword = async (
updatePassword: TypesGen.UpdateUserPasswordRequest,
): Promise<undefined> => axios.put(`/api/v2/users/${userId}/password`, updatePassword)
export const getSiteRoles = async (): Promise<Array<TypesGen.Role>> => {
const response = await axios.get<Array<TypesGen.Role>>(`/api/v2/users/roles`)
export const getSiteRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => {
const response = await axios.get<Array<TypesGen.AssignableRoles>>(`/api/v2/users/roles`)
return response.data
}

View File

@ -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

View File

@ -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<RoleSelectProps> = (args) => <RoleSelect {...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],
}

View File

@ -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(
<RoleSelect
roles={[
assignableRole(MockOwnerRole, false),
assignableRole(MockTemplateAdminRole, false),
assignableRole(MockAuditorRole, true),
assignableRole(MockUserAdminRole, true),
]}
selectedRoles={[MockUserAdminRole, MockTemplateAdminRole, MockMemberRole]}
loading={false}
onChange={jest.fn()}
open
/>,
)
// 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")
})
})

View File

@ -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<RoleSelectProps> = ({
const isChecked = selectedRoles.some((selectedRole) => selectedRole.name === r.name)
return (
<MenuItem key={r.name} value={r.name} disabled={loading}>
<MenuItem key={r.name} value={r.name} disabled={loading || !r.assignable}>
<Checkbox size="small" color="primary" checked={isChecked} /> {r.display_name}
</MenuItem>
)

View File

@ -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(<UserDropdownContent user={MockUser} onSignOut={jest.fn()} onPopoverClose={jest.fn()} />)
expect(screen.getByText(MockAdminRole.display_name)).toBeDefined()
expect(screen.getByText(MockOwnerRole.display_name)).toBeDefined()
})
it("has the correct link for the account item", () => {

View File

@ -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

View File

@ -20,7 +20,7 @@ export const Language = {
interface UsersTableBodyProps {
users?: TypesGen.User[]
roles?: TypesGen.Role[]
roles?: TypesGen.AssignableRoles[]
isUpdatingUserRoles?: boolean
canEditUsers?: boolean
isLoading?: boolean

View File

@ -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)

View File

@ -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

View File

@ -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 = {

View File

@ -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[],
},
},
},