Skip to content
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
4 changes: 4 additions & 0 deletions src/authz-module/audit-user/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jest.mock('@edx/frontend-component-header', () => ({
jest.mock('@src/data/hooks', () => ({
...jest.requireActual('@src/data/hooks'),
useUserAccount: jest.fn(),
useValidateUserPermissionsNonSuspense: jest.fn().mockReturnValue({
data: [{ scope: 'lib:test', allowed: true }],
isLoading: false,
}),
}));

// Mock the useRevokeUserRoles hook
Expand Down
38 changes: 30 additions & 8 deletions src/authz-module/audit-user/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import {
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import type { AppContextType } from '@edx/frontend-platform/react';
import debounce from 'lodash.debounce';
import {
Container, DataTable,
} from '@openedx/paragon';
import TableFooter from '@src/authz-module/components/TableFooter/TableFooter';
import { AUTHZ_HOME_PATH, TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
import {
AUTHZ_HOME_PATH, TABLE_DEFAULT_PAGE_SIZE,
} from '@src/authz-module/constants';
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
import { useNavigate, useParams } from 'react-router-dom';
import { useUserAccount } from '@src/data/hooks';
import { useUserAccount, useValidateUserPermissionsNonSuspense } from '@src/data/hooks';
import baseMessages from '@src/authz-module/messages';
import AddRoleButton from '@src/authz-module/components/AddRoleButton';
import {
Expand All @@ -30,7 +31,7 @@ import RolesFilter from '@src/authz-module/components/TableControlBar/RolesFilte
import TableControlBar from '@src/authz-module/components/TableControlBar/TableControlBar';
import messages from './messages';
import ConfirmDeletionModal from '../components/ConfirmDeletionModal';
import { getCellHeader } from '../components/utils';
import { getCellHeader, getScopeManageActionPermission } from '../utils';

const AuditUserPage = () => {
const { formatMessage } = useIntl();
Expand All @@ -52,7 +53,30 @@ const AuditUserPage = () => {
} = useToastManager();
const { mutate: revokeUserRoles, isPending: isRevokingUserRolePending } = useRevokeUserRoles();

const fetchData = useMemo(() => debounce(handleTableFetch, 500), [handleTableFetch]);
const deletePermissions = useMemo(() => {
const uniqueScopes = [...new Set(userAssignments.map(assignment => assignment.scope))];
return uniqueScopes.map(scope => getScopeManageActionPermission(scope));
}, [userAssignments]);

const {
data: permissionsToManageScope,
} = useValidateUserPermissionsNonSuspense(deletePermissions);

const rowsWithPermissions = useMemo(() => {
if (!permissionsToManageScope) { return userAssignments; }

return userAssignments.map(assignment => {
const canManageScope = permissionsToManageScope.some(
permission => permission.scope === assignment.scope && permission.allowed,
);
return {
...assignment,
canManageScope,
};
});
}, [userAssignments, permissionsToManageScope]);

const fetchData = useMemo(() => handleTableFetch, [handleTableFetch]);

useEffect(() => {
if (!user && !isLoadingUser) {
Expand All @@ -63,8 +87,6 @@ const AuditUserPage = () => {
}
}, [user, isLoadingUser, navigate, isErrorUser, errorUser]);

useEffect(() => () => fetchData.cancel(), [fetchData]);

const handleShowConfirmDeletionModal = useCallback((role: RoleToDelete) => {
if (isRevokingUserRolePending) { return; }

Expand Down Expand Up @@ -227,7 +249,7 @@ const AuditUserPage = () => {
isFilterable
isSortable
manualPagination
data={userAssignments}
data={rowsWithPermissions}
manualFilters
manualSortBy
fetchData={fetchData}
Expand Down
49 changes: 42 additions & 7 deletions src/authz-module/components/TableCells.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ import {
createActionsCell,
} from './TableCells';

// TODO: remove console.log mocks and implement actual logic for these cells, then update tests accordingly
// Mock console.log for TODO functions
jest.spyOn(console, 'log').mockImplementation(() => {});

const mockNavigate = jest.fn();

jest.mock('react-router-dom', () => ({
Expand Down Expand Up @@ -490,6 +486,7 @@ describe('TableCells Components', () => {
org: 'Test Org',
scope: 'Test Scope',
permissionCount: 1,
canManageScope: true,
},
};

Expand All @@ -512,7 +509,26 @@ describe('TableCells Components', () => {
expect(mockOnClickDeleteButton).toHaveBeenCalledWith({ name: 'Library Admin', role: 'library_admin', scope: 'Test Scope' });
});

it('renders a disabled button for admin roles when isUserAuthenticatedPage is true', () => {
it('renders a disabled delete icon for admin roles when isUserAuthenticatedPage is true', () => {
const adminRow = {
original: {
role: 'course_admin',
org: 'Test Org',
scope: 'Test Scope',
permissionCount: 1,
},
};
const CustomActionsCell = createActionsCell({
onClickDeleteButton: mockOnClickDeleteButton,
isUserAuthenticatedPage: true,
});
renderWrapper(<CustomActionsCell row={adminRow} column={{ id: 'actions' }} />);

const infoIcon = screen.getByRole('img', { hidden: true });
expect(infoIcon).toBeInTheDocument();
});

it('renders a tooltip when hovering over delete icon for admin roles when isUserAuthenticatedPage is true', async () => {
const adminRow = {
original: {
role: 'course_admin',
Expand All @@ -521,14 +537,16 @@ describe('TableCells Components', () => {
permissionCount: 1,
},
};
const user = userEvent.setup();
const CustomActionsCell = createActionsCell({
onClickDeleteButton: mockOnClickDeleteButton,
isUserAuthenticatedPage: true,
});
renderWrapper(<CustomActionsCell row={adminRow} column={{ id: 'actions' }} />);

const button = screen.getByRole('button', { name: /delete role action/i });
expect(button).toBeDisabled();
const infoIcon = screen.getByRole('img', { hidden: true });
await user.hover(infoIcon);
expect(screen.getByText(/You can’t remove your own admin role/i)).toBeInTheDocument();
});

it('renders info icon with tooltip for Django managed roles', async () => {
Expand All @@ -552,6 +570,23 @@ describe('TableCells Components', () => {
await user.hover(infoIcon);
expect(screen.getByText(/Please go to Django Admin to manage it/i)).toBeInTheDocument();
});

it('renders a disabled button when user does not have permission', async () => {
const CustomActionsCell = createActionsCell({
onClickDeleteButton: mockOnClickDeleteButton,
isUserAuthenticatedPage: false,
});
const customRow = {
original: {
...baseRow,
canManageScope: false,
},
};
renderWrapper(<CustomActionsCell row={customRow} column={{ id: 'actions' }} />);

const deleteButton = screen.queryByRole('button', { name: /delete role action/i });
expect(deleteButton).toBeDisabled();
});
});

describe('ViewAllPermissionsCell', () => {
Expand Down
43 changes: 29 additions & 14 deletions src/authz-module/components/TableCells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
Info,
} from '@openedx/paragon/icons';
import {
TableCellValue, AppContextType, UserRole, RoleToDelete,
TableCellValue, AppContextType, UserRoleWithPermissions, RoleToDelete,
} from '@src/types';
import { useNavigate } from 'react-router-dom';
import { useContext, useMemo } from 'react';
import { ADMIN_ROLES, DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL } from '@src/authz-module/constants';
import {
ADMIN_ROLES, DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL,
} from '@src/authz-module/constants';
import {
Icon, IconButton, OverlayTrigger, Tooltip, DataTableContext,
} from '@openedx/paragon';
Expand All @@ -25,7 +27,7 @@ interface DataTableInstance {
toggleRowExpanded?: (rowId: string, expanded: boolean) => void;
}

type CellProps = TableCellValue<UserRole>;
type CellProps = TableCellValue<UserRoleWithPermissions>;
type CellPropsWithValue = CellProps & {
value: string;
};
Expand Down Expand Up @@ -160,9 +162,11 @@ const ViewAllPermissionsCell = ({ row }: CellProps) => {
);
};

const ActionsCell = ({ row, onClickDeleteButton, isUserAuthenticatedPage }: ActionsCellProps) => {
const ActionsCell = ({
row, onClickDeleteButton, isUserAuthenticatedPage,
}: ActionsCellProps) => {
const { formatMessage } = useIntl();
const { role } = row.original;
const { role, canManageScope } = row.original;

const handleDelete = () => {
const roleToDelete = {
Expand Down Expand Up @@ -193,19 +197,30 @@ const ActionsCell = ({ row, onClickDeleteButton, isUserAuthenticatedPage }: Acti

if (ADMIN_ROLES.includes(role) && isUserAuthenticatedPage) {
return (
<IconButton
// @ts-ignore
disabled
isActive={false}
variant="light"
alt={formatMessage(messages['authz.user.table.delete.action.alt'])}
src={Delete}
/>
<OverlayTrigger
placement="left"
overlay={(
<Tooltip variant="light" id="tooltip-left">
{formatMessage(messages['authz.user.table.delete.action.adminrole.tooltip'])}
</Tooltip>
)}
>
<Icon
className="mx-2 pl-1 text-light-500"
src={Delete}
/>
</OverlayTrigger>
);
}

return (
<IconButton variant="danger" onClick={handleDelete} alt={formatMessage(messages['authz.user.table.delete.action.alt'])} src={Delete} />
<IconButton
disabled={!canManageScope}
variant={canManageScope ? 'danger' : 'light'}
onClick={handleDelete}
alt={formatMessage(messages['authz.user.table.delete.action.alt'])}
src={Delete}
/>
);
};

Expand Down
5 changes: 5 additions & 0 deletions src/authz-module/components/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ const messages = defineMessages({
defaultMessage: 'You can’t remove this role here. Please go to Django Admin to manage it.',
description: 'Tooltip for delete button when hovering over Django roles',
},
'authz.user.table.delete.action.adminrole.tooltip': {
id: 'authz.user.table.delete.action.adminrole.tooltip',
defaultMessage: 'You can’t remove your own admin role. This prevents a resource from being left without an admin. Another user with the required permissions can revoke it.',
description: 'Tooltip for delete button when hovering over Admin roles',
},
'authz.user.table.view_all_permissions.link.text.close': {
id: 'authz.user.table.view_all_permissions.link.text.close',
defaultMessage: 'Hide all permissions',
Expand Down
84 changes: 0 additions & 84 deletions src/authz-module/components/utils.test.tsx

This file was deleted.

14 changes: 0 additions & 14 deletions src/authz-module/components/utils.tsx

This file was deleted.

Loading