Skip to content
Open
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
8 changes: 4 additions & 4 deletions src/authz-module/audit-user/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@openedx/paragon';
import TableFooter from '@src/authz-module/components/TableFooter/TableFooter';
import {
AUTHZ_HOME_PATH, TABLE_DEFAULT_PAGE_SIZE,
ROUTES, TABLE_DEFAULT_PAGE_SIZE,
} from '@src/authz-module/constants';
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
import { useNavigate, useParams } from 'react-router-dom';
Expand Down Expand Up @@ -81,7 +81,7 @@ const AuditUserPage = () => {
if (!user && !isLoadingUser) {
// @ts-ignore
if (!isErrorUser || errorUser?.customAttributes?.httpErrorStatus === 404) {
navigate(AUTHZ_HOME_PATH);
navigate(ROUTES.HOME_PATH);
}
}
}, [user, isLoadingUser, navigate, isErrorUser, errorUser]);
Expand All @@ -96,7 +96,7 @@ const AuditUserPage = () => {
const navLinks = useMemo(() => [
{
label: formatMessage(baseMessages['authz.management.home.nav.link']),
to: AUTHZ_HOME_PATH,
to: ROUTES.HOME_PATH,
},
], [formatMessage]);

Expand Down Expand Up @@ -202,7 +202,7 @@ const AuditUserPage = () => {
});
handleCloseConfirmDeletionModal();
if (remainingRolesCount === 0) {
navigate(AUTHZ_HOME_PATH);
navigate(ROUTES.HOME_PATH);
}
},
onError: (error, retryVariables) => {
Expand Down
6 changes: 3 additions & 3 deletions src/authz-module/components/PermissionTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const mockRoles: Role[] = [
userCount: 0,
permissions: [],
role: '',
contextType: '',
contextType: 'course',
scope: '',
},
{
Expand All @@ -19,7 +19,7 @@ const mockRoles: Role[] = [
userCount: 0,
permissions: [],
role: '',
contextType: '',
contextType: 'course',
scope: '',
},
{
Expand All @@ -28,7 +28,7 @@ const mockRoles: Role[] = [
userCount: 0,
permissions: [],
role: '',
contextType: '',
contextType: 'course',
scope: '',
},
];
Expand Down
14 changes: 7 additions & 7 deletions src/authz-module/components/RenderAdminRole.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import RenderAdminRole from './RenderAdminRole';
describe('RenderAdminRole', () => {
const adminRole = 'course_admin';
const superuserRole = 'django.superuser';
const staffRole = 'django.globalstaff';
const staffRole = 'django.staff';
const instructorRole = 'instructor';
const emptyRole = '';
const mixedCaseAdminRole = 'Library_Admin';
Expand All @@ -27,13 +27,13 @@ describe('RenderAdminRole', () => {
expect(container.querySelector('.mb-0')).toBeInTheDocument();
});

it('displays admin message for roles containing admin', () => {
renderWrapper(<RenderAdminRole role={adminRole} />);
it('displays admin message for superuser role', () => {
renderWrapper(<RenderAdminRole role={superuserRole} />);
expect(screen.getByText(/super admins have full access/i)).toBeInTheDocument();
});

it('displays staff message for superuser role', () => {
renderWrapper(<RenderAdminRole role={superuserRole} />);
it('displays staff message for non-django admin roles', () => {
renderWrapper(<RenderAdminRole role={adminRole} />);
expect(screen.getByText(/global staff have access/i)).toBeInTheDocument();
});

Expand All @@ -57,9 +57,9 @@ describe('RenderAdminRole', () => {
expect(screen.getByText(/global staff have access/i)).toBeInTheDocument();
});

it('displays admin message for mixed case admin role', () => {
it('displays staff message for mixed case admin role', () => {
renderWrapper(<RenderAdminRole role={mixedCaseAdminRole} />);
expect(screen.getByText(/super admins have full access/i)).toBeInTheDocument();
expect(screen.getByText(/global staff have access/i)).toBeInTheDocument();
});

it('displays staff message for regular role without admin', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/authz-module/components/RenderAdminRole.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { SUPERUSER_ROLE } from '@src/authz-module/constants';
import messages from '@src/authz-module/audit-user/messages';

interface RenderAdminRoleProps {
Expand All @@ -7,8 +8,7 @@ interface RenderAdminRoleProps {

const RenderAdminRole = ({ role }: RenderAdminRoleProps) => {
const intl = useIntl();
// Determine which message to show based on role
const messageKey = role?.toLowerCase().includes('admin')

@dcoa dcoa Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is a small fix for the release too
Current message (Verawood sandbox)

Image

After the change

Image

@MaferMazu

const messageKey = role === SUPERUSER_ROLE
? 'authz.user.table.permissions.role.admin'
: 'authz.user.table.permissions.role.staff';

Expand Down
8 changes: 4 additions & 4 deletions src/authz-module/components/TableCells.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe('TableCells Components', () => {
const viewButton = screen.getByRole('button', { name: /view/i });
await user.click(viewButton);

expect(mockNavigate).toHaveBeenCalledWith('/authz/user/user+with@special.chars');
expect(mockNavigate).toHaveBeenCalledWith(`/authz/user/${encodeURIComponent('user+with@special.chars')}`);
});
});

Expand Down Expand Up @@ -353,7 +353,7 @@ describe('TableCells Components', () => {
row: {
id: '0',
original: {
role: 'django.globalstaff', org: 'Test Org', scope: 'Test Scope', permissionCount: 1,
role: 'django.staff', org: 'Test Org', scope: 'Test Scope', permissionCount: 1,
},
},
column: { id: 'org' },
Expand Down Expand Up @@ -409,7 +409,7 @@ describe('TableCells Components', () => {
row: {
id: '0',
original: {
role: 'django.globalstaff', org: 'Test Org', scope: 'Test Scope', permissionCount: 1,
role: 'django.staff', org: 'Test Org', scope: 'Test Scope', permissionCount: 1,
},
},
column: { id: 'scope' },
Expand Down Expand Up @@ -465,7 +465,7 @@ describe('TableCells Components', () => {
row: {
id: '0',
original: {
role: 'django.globalstaff',
role: 'django.staff',
permissionCount: 5,
org: 'Test Org',
scope: 'Test Scope',
Expand Down
9 changes: 5 additions & 4 deletions src/authz-module/components/TableCells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { 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,
ADMIN_ROLES, buildUserPath, DJANGO_MANAGED_ROLES, getScopeContextType,
MAP_ROLE_KEY_TO_LABEL, SUPERUSER_ROLE,
} from '@src/authz-module/constants';
import {
Icon, IconButton, OverlayTrigger, Tooltip, DataTableContext,
Expand Down Expand Up @@ -62,7 +63,7 @@ const NameCell = ({ row }: CellProps) => {
const ViewActionCell = ({ row }: CellProps) => {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const viewPath = `/authz/user/${row.original.username}`;
const viewPath = buildUserPath(row.original.username ?? '');
return (
<IconButton
src={RemoveRedEye}
Expand Down Expand Up @@ -92,7 +93,7 @@ const ScopeCell = ({ row }: CellProps) => {
iconSrc: RESOURCE_ICONS.GLOBAL,
};
}
const scopeIcon = row.original.role?.startsWith('lib') ? RESOURCE_ICONS.LIBRARY : RESOURCE_ICONS.COURSE;
const scopeIcon = getScopeContextType(row.original.scope) === 'library' ? RESOURCE_ICONS.LIBRARY : RESOURCE_ICONS.COURSE;
return {
scopeText: row.original.scope,
iconSrc: scopeIcon,
Expand Down Expand Up @@ -125,7 +126,7 @@ const PermissionsCell = ({ row }: CellProps) => {
{ isDjangoRole
? formatMessage(
messages['authz.user.table.permissions.access.label'],
{ accessType: role === 'django.superuser' ? 'total' : 'partial' },
{ accessType: role === SUPERUSER_ROLE ? 'total' : 'partial' },
)
: formatMessage(messages['authz.user.table.permissions.available.count'], { count })}
</span>
Expand Down
100 changes: 35 additions & 65 deletions src/authz-module/components/constants.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,44 @@
import type { IntlShape } from '@edx/frontend-platform/i18n';
import { Language, LibraryBooks, School } from '@openedx/paragon/icons';
import { allRolesMetadata } from '@src/authz-module/roles-permissions';
import { GLOBAL_STAFF_ROLE, MAP_ROLE_KEY_TO_LABEL, SUPERUSER_ROLE } from '@src/authz-module/constants';
import messages from './messages';

export const getRolesFiltersOptions = (intl: IntlShape) => [
{
groupName: intl.formatMessage(messages['authz.team.members.table.group.global']),
groupIcon: Language,
displayName: 'Super Admin',
value: 'super_admin',
},
{
groupName: intl.formatMessage(messages['authz.team.members.table.group.global']),
groupIcon: Language,
displayName: 'Global Staff',
value: 'global_staff',
},

{
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
groupIcon: School,
displayName: 'Course Admin',
value: 'course_admin',
},
{
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
groupIcon: School,
displayName: 'Course Staff',
value: 'course_staff',
},
{
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
groupIcon: School,
displayName: 'Course Editor',
value: 'course_editor',
},
{
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
groupIcon: School,
displayName: 'Course Auditor',
value: 'course_auditor',
},

{
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
groupIcon: LibraryBooks,
displayName: 'Library Admin',
value: 'library_admin',
},
{
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
groupIcon: LibraryBooks,
displayName: 'Library Author',
value: 'library_author',
},
{
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
groupIcon: LibraryBooks,
displayName: 'Library Contributor',
value: 'library_contributor',
},
{
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
groupIcon: LibraryBooks,
displayName: 'Library User',
value: 'library_user',
},
];

export const RESOURCE_ICONS = {
COURSE: School,
LIBRARY: LibraryBooks,
GLOBAL: Language,
};

// The API expects the underscore format when roles are sent as filter values,
// while role data received from the API uses the dotted format (e.g. django.superuser).
const GLOBAL_ROLE_FILTER_OPTIONS = [
{ value: 'super_admin', displayName: MAP_ROLE_KEY_TO_LABEL[SUPERUSER_ROLE] },
{ value: 'global_staff', displayName: MAP_ROLE_KEY_TO_LABEL[GLOBAL_STAFF_ROLE] },
];

export const getRolesFiltersOptions = (intl: IntlShape) => {
const globalGroup = {
groupName: intl.formatMessage(messages['authz.team.members.table.group.global']),
groupIcon: RESOURCE_ICONS.GLOBAL,
};
const contextGroups = {
course: {
groupName: intl.formatMessage(messages['authz.team.members.table.group.courses']),
groupIcon: RESOURCE_ICONS.COURSE,
},
library: {
groupName: intl.formatMessage(messages['authz.team.members.table.group.libraries']),
groupIcon: RESOURCE_ICONS.LIBRARY,
},
};

return [
...GLOBAL_ROLE_FILTER_OPTIONS.map((role) => ({ ...globalGroup, ...role })),
...allRolesMetadata.map((meta) => ({
...contextGroups[meta.contextType],
displayName: meta.name,
value: meta.role,
})),
];
};
51 changes: 15 additions & 36 deletions src/authz-module/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const ORG_AGGREGATE_SCOPE_BUILDERS = {
import type { ContextType } from '@src/types';
import { allRolesMetadata } from './roles-permissions';

const ORG_AGGREGATE_SCOPE_BUILDERS: Record<ContextType, (orgSlug: string) => string> = {
course: (orgSlug: string) => `course-v1:${orgSlug}+*`,
library: (orgSlug: string) => `lib:${orgSlug}:*`,
};
Expand All @@ -9,21 +12,19 @@ export const getOrgAggregateScopeKey = (contextType: string, orgSlug: string): s
return builder(orgSlug);
};

export const getScopeContextType = (scope: string): ContextType => (scope.startsWith('lib') ? 'library' : 'course');

export const DEFAULT_TOAST_DELAY = 5000;
export const RETRY_TOAST_DELAY = 120_000; // 2 minutes
export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
username: 'skeleton',
name: '',
email: '',
roles: [],
}));

export const ROUTES = {
HOME_PATH: '/authz',
AUDIT_USER_PATH: '/user/:username',
ASSIGN_ROLE_WIZARD_PATH: '/assign-role',
};

export const buildUserPath = (username: string) => `${ROUTES.HOME_PATH}${ROUTES.AUDIT_USER_PATH.replace(':username', encodeURIComponent(username))}`;

export const buildWizardPath = (options?: { users?: string; from?: string }) => {
const base = `${ROUTES.HOME_PATH}${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`;
if (!options) { return base; }
Expand All @@ -34,42 +35,20 @@ export const buildWizardPath = (options?: { users?: string; from?: string }) =>
return query ? `${base}?${query}` : base;
};

export enum RoleOperationErrorStatus {
USER_NOT_FOUND = 'user_not_found',
USER_ALREADY_HAS_ROLE = 'user_already_has_role',
USER_DOES_NOT_HAVE_ROLE = 'user_does_not_have_role',
ROLE_ASSIGNMENT_ERROR = 'role_assignment_error',
ROLE_REMOVAL_ERROR = 'role_removal_error',
}

export const MAX_TABLE_FILTERS_APPLIED = 10;

export const AUTHZ_HOME_PATH = '/authz';
// Role data received from the API uses the dotted format for Django-managed roles.
export const SUPERUSER_ROLE = 'django.superuser';
export const GLOBAL_STAFF_ROLE = 'django.staff';

@dcoa dcoa Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export const DJANGO_MANAGED_ROLES = [SUPERUSER_ROLE, GLOBAL_STAFF_ROLE];

export const MAP_ROLE_KEY_TO_LABEL: Record<string, string> = {
library_admin: 'Library Admin',
library_author: 'Library Author',
library_contributor: 'Library Contributor',
library_user: 'Library User',
course_admin: 'Course Admin',
course_staff: 'Course Staff',
course_editor: 'Course Editor',
course_auditor: 'Course Auditor',
'django.superuser': 'Super Admin',
'django.globalstaff': 'Global Staff',
...Object.fromEntries(allRolesMetadata.map((meta) => [meta.role, meta.name])),
[SUPERUSER_ROLE]: 'Super Admin',
[GLOBAL_STAFF_ROLE]: 'Global Staff',
};

export const DJANGO_MANAGED_ROLES = ['django.superuser', 'django.globalstaff'];

export const TABLE_DEFAULT_PAGE_SIZE = 10;

export const DEFAULT_FILTER_PAGE_SIZE = 5;
export const ADMIN_ROLES = ['course_admin', 'library_admin'];

// Resource Type Definitions
export const RESOURCE_TYPES = {
LIBRARY: 'library',
COURSE: 'course',
} as const;

export type ResourceType = typeof RESOURCE_TYPES[keyof typeof RESOURCE_TYPES];
Loading