Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Conditionally render User edit/delete actions in the web UI #49645

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions web/packages/teleport/src/Users/UserList/UserList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import React from 'react';
import { Cell, LabelCell } from 'design/DataTable';
import { MenuButton, MenuItem } from 'shared/components/MenuAction';

import { User, UserOrigin } from 'teleport/services/user';
import { Access, User, UserOrigin } from 'teleport/services/user';
import { ClientSearcheableTableWithQueryParamSupport } from 'teleport/components/ClientSearcheableTableWithQueryParamSupport';

export default function UserList({
Expand All @@ -29,6 +29,7 @@ export default function UserList({
onEdit,
onDelete,
onReset,
usersAcl,
}: Props) {
return (
<ClientSearcheableTableWithQueryParamSupport
Expand Down Expand Up @@ -72,6 +73,7 @@ export default function UserList({
altKey: 'options-btn',
render: user => (
<ActionCell
acl={usersAcl}
user={user}
onEdit={onEdit}
onReset={onReset}
Expand Down Expand Up @@ -118,24 +120,37 @@ const ActionCell = ({
onEdit,
onReset,
onDelete,
acl,
}: {
user: User;
onEdit: (user: User) => void;
onReset: (user: User) => void;
onDelete: (user: User) => void;
acl: Access;
}) => {
const canEdit = acl.edit;
const canDelete = acl.remove;

if (!(canEdit || canDelete)) {
return <Cell align="right" />;
}

if (user.isBot || !user.isLocal) {
return <Cell align="right" />;
}

return (
<Cell align="right">
<MenuButton>
<MenuItem onClick={() => onEdit(user)}>Edit...</MenuItem>
<MenuItem onClick={() => onReset(user)}>
Reset Authentication...
</MenuItem>
<MenuItem onClick={() => onDelete(user)}>Delete...</MenuItem>
{canEdit && <MenuItem onClick={() => onEdit(user)}>Edit...</MenuItem>}
{canEdit && (
<MenuItem onClick={() => onReset(user)}>
Reset Authentication...
</MenuItem>
)}
{canDelete && (
<MenuItem onClick={() => onDelete(user)}>Delete...</MenuItem>
)}
</MenuButton>
</Cell>
);
Expand All @@ -147,4 +162,7 @@ type Props = {
onEdit(user: User): void;
onDelete(user: User): void;
onReset(user: User): void;
// determines if the viewer is able to edit/delete users. This is used
// to conditionally render the edit/delete buttons in the ActionCell
usersAcl: Access;
};
8 changes: 8 additions & 0 deletions web/packages/teleport/src/Users/Users.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,12 @@ const sample = {
EmailPasswordReset: null,
showMauInfo: false,
onDismissUsersMauNotice: () => null,
canEditUsers: true,
usersAcl: {
read: true,
edit: false,
remove: true,
list: true,
create: true,
},
};
169 changes: 168 additions & 1 deletion web/packages/teleport/src/Users/Users.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,23 @@

import React from 'react';
import { MemoryRouter } from 'react-router';
import { render, screen, userEvent } from 'design/utils/testing';
import { render, screen, userEvent, fireEvent } from 'design/utils/testing';

import { ContextProvider } from 'teleport';
import { createTeleportContext } from 'teleport/mocks/contexts';
import { Access } from 'teleport/services/user';

import { Users } from './Users';
import { State } from './useUsers';

const defaultAcl: Access = {
read: true,
edit: true,
remove: true,
list: true,
create: true,
};

describe('invite collaborators integration', () => {
const ctx = createTeleportContext();

Expand Down Expand Up @@ -59,6 +68,7 @@ describe('invite collaborators integration', () => {
EmailPasswordReset: null,
showMauInfo: false,
onDismissUsersMauNotice: () => null,
usersAcl: defaultAcl,
};
});

Expand Down Expand Up @@ -138,6 +148,7 @@ test('Users not equal to MAU Notice', async () => {
EmailPasswordReset: null,
showMauInfo: true,
onDismissUsersMauNotice: jest.fn(),
usersAcl: defaultAcl,
};

const user = userEvent.setup();
Expand Down Expand Up @@ -192,6 +203,7 @@ describe('email password reset integration', () => {
EmailPasswordReset: null,
showMauInfo: false,
onDismissUsersMauNotice: () => null,
usersAcl: defaultAcl,
};
});

Expand Down Expand Up @@ -232,3 +244,158 @@ describe('email password reset integration', () => {
expect(screen.getByTestId('new-reset-ui')).toBeInTheDocument();
});
});

describe('permission handling', () => {
const ctx = createTeleportContext();

let props: State;
beforeEach(() => {
props = {
attempt: {
message: 'success',
isSuccess: true,
isProcessing: false,
isFailed: false,
},
users: [
{
name: 'tester',
roles: [],
isLocal: true,
},
],
fetchRoles: () => Promise.resolve([]),
operation: {
type: 'reset',
user: { name: '[email protected]', roles: ['foo'] },
},

onStartCreate: () => undefined,
onStartDelete: () => undefined,
onStartEdit: () => undefined,
onStartReset: () => undefined,
onStartInviteCollaborators: () => undefined,
onClose: () => undefined,
onDelete: () => undefined,
onCreate: () => undefined,
onUpdate: () => undefined,
onReset: () => undefined,
onInviteCollaboratorsClose: () => undefined,
InviteCollaborators: null,
inviteCollaboratorsOpen: false,
onEmailPasswordResetClose: () => undefined,
EmailPasswordReset: null,
showMauInfo: false,
onDismissUsersMauNotice: () => null,
usersAcl: defaultAcl,
};
});

test('displays a disabled Create Users button if lacking permissions', async () => {
const testProps = {
...props,
usersAcl: {
...defaultAcl,
edit: false,
},
};
render(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<Users {...testProps} />
</ContextProvider>
</MemoryRouter>
);

expect(screen.getByTestId('create_new_users_button')).toBeDisabled();
});

test('edit and reset options not available in the menu', async () => {
const testProps = {
...props,
usersAcl: {
...defaultAcl,
edit: false,
},
};
render(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<Users {...testProps} />
</ContextProvider>
</MemoryRouter>
);

const optionsButton = screen.getByRole('button', { name: /options/i });
fireEvent.click(optionsButton);
const menuItems = screen.queryAllByRole('menuitem');
expect(menuItems).toHaveLength(1);
expect(menuItems.some(item => item.textContent.includes('Delete'))).toBe(
true
);
});

test('all options are available in the menu', async () => {
const testProps = {
...props,
usersAcl: {
read: true,
list: true,
edit: true,
create: true,
remove: true,
},
};
render(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<Users {...testProps} />
</ContextProvider>
</MemoryRouter>
);

expect(screen.getByText('tester')).toBeInTheDocument();
const optionsButton = screen.getByRole('button', { name: /options/i });
fireEvent.click(optionsButton);
const menuItems = screen.queryAllByRole('menuitem');
expect(menuItems).toHaveLength(3);
expect(menuItems.some(item => item.textContent.includes('Delete'))).toBe(
true
);
expect(
menuItems.some(item => item.textContent.includes('Reset Auth'))
).toBe(true);
expect(menuItems.some(item => item.textContent.includes('Edit'))).toBe(
true
);
});

test('delete is not available in menu', async () => {
const testProps = {
...props,
usersAcl: {
read: true,
list: true,
edit: true,
create: true,
remove: false,
},
};
render(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<Users {...testProps} />
</ContextProvider>
</MemoryRouter>
);

expect(screen.getByText('tester')).toBeInTheDocument();
const optionsButton = screen.getByRole('button', { name: /options/i });
fireEvent.click(optionsButton);
const menuItems = screen.queryAllByRole('menuitem');
expect(menuItems).toHaveLength(2);
expect(
menuItems.every(item => item.textContent.includes('Delete'))
).not.toBe(true);
});
});
Loading
Loading