feat: Add validation for Advanced Settings permissions using openedx-authz (#2869)

* feat: Add validation for Advanced Settings permissions using openedx-authz

* squash!: Increase test coverage

* squash!: Fix lint issues

* squash!: Validate advanced settings permission on HelpSidebar

* squash!: Increase test coverage

* squash!: Attend PR comments
This commit is contained in:
Rodrigo Mendez
2026-02-19 14:39:04 -06:00
committed by GitHub
parent 42f26e7404
commit 5ccf39d130
8 changed files with 265 additions and 27 deletions

View File

@@ -6,6 +6,10 @@ import {
import { CheckCircle, Info, Warning } from '@openedx/paragon/icons';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { useWaffleFlags } from '@src/data/apiHooks';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { COURSE_PERMISSIONS } from '@src/authz/constants';
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
import Placeholder from '../editors/Placeholder';
import AlertProctoringError from '../generic/AlertProctoringError';
@@ -41,6 +45,15 @@ const AdvancedSettings = () => {
const { courseId, courseDetails } = useCourseAuthoringContext();
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
const waffleFlags = useWaffleFlags(courseId);
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
canManageAdvancedSettings: {
action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
scope: courseId,
},
}, isAuthzEnabled);
useEffect(() => {
dispatch(fetchCourseAppSettings(courseId));
dispatch(fetchProctoringExamErrors(courseId));
@@ -52,7 +65,7 @@ const AdvancedSettings = () => {
const settingsWithSendErrors = useSelector(getSendRequestErrors) || {};
const loadingSettingsStatus = useSelector(getLoadingStatus);
const isLoading = loadingSettingsStatus === RequestStatus.IN_PROGRESS;
const isLoading = loadingSettingsStatus === RequestStatus.IN_PROGRESS || (isAuthzEnabled && isLoadingUserPermissions);
const updateSettingsButtonState = {
labels: {
default: intl.formatMessage(messages.buttonSaveText),
@@ -128,6 +141,15 @@ const AdvancedSettings = () => {
showSaveSettingsPrompt(true);
};
// Show permission denied alert when authz is enabled and user doesn't have permission
const authzIsEnabledAndNoPermission = isAuthzEnabled
&& !isLoadingUserPermissions
&& !userPermissions?.canManageAdvancedSettings;
if (authzIsEnabledAndNoPermission) {
return <PermissionDeniedAlert />;
}
return (
<>
<Container size="xl" className="advanced-settings px-4">
@@ -192,8 +214,8 @@ const AdvancedSettings = () => {
defaultMessage="{visibility} deprecated settings"
values={{
visibility:
showDeprecated ? intl.formatMessage(messages.deprecatedButtonHideText)
: intl.formatMessage(messages.deprecatedButtonShowText),
showDeprecated ? intl.formatMessage(messages.deprecatedButtonHideText)
: intl.formatMessage(messages.deprecatedButtonShowText),
}}
/>
</Button>

View File

@@ -1,4 +1,6 @@
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
import {
render as baseRender,
fireEvent,
@@ -21,11 +23,15 @@ const courseId = '123';
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
<textarea
{...props}
onFocus={() => {}}
onBlur={() => {}}
onFocus={() => { }}
onBlur={() => { }}
/>
)));
jest.mock('@src/authz/data/apiHooks', () => ({
useUserPermissions: jest.fn(),
}));
const render = () => baseRender(
<CourseAuthoringProvider courseId={courseId}>
<AdvancedSettings />
@@ -41,6 +47,11 @@ describe('<AdvancedSettings />', () => {
axiosMock
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
.reply(200, advancedSettingsMock);
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: true },
});
});
it('should render without errors', async () => {
const { getByText } = render();
@@ -144,4 +155,38 @@ describe('<AdvancedSettings />', () => {
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
expect(getByText('Your policy changes have been saved.')).toBeInTheDocument();
});
it('should render without errors when authz.enable_course_authoring flag is enabled and the user is authorized', async () => {
// Mock feature flag
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: true },
});
const { getByText } = render();
await waitFor(() => {
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
const advancedSettingsElement = getByText(messages.headingTitle.defaultMessage, {
selector: 'h2.sub-header-title',
});
expect(advancedSettingsElement).toBeInTheDocument();
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
expect(getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
});
});
it('should show permission alert when authz.enable_course_authoring flag is enabled and the user is not authorized', async () => {
// Mock feature flag
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: false },
});
const { getByTestId } = render();
await waitFor(() => {
const permissionAlert = getByTestId('permissionDeniedAlert');
expect(permissionAlert).toBeInTheDocument();
});
});
});

View File

@@ -14,3 +14,7 @@ export const CONTENT_LIBRARY_PERMISSIONS = {
MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team',
VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team',
};
export const COURSE_PERMISSIONS = {
MANAGE_ADVANCED_SETTINGS: 'courses.manage_advanced_settings',
};

View File

@@ -92,6 +92,7 @@ export const waffleFlagDefaults = {
useNewGroupConfigurationsPage: true,
useReactMarkdownEditor: true,
useVideoGalleryFlow: false,
enableAuthzCourseAuthoring: false,
} as const;
export type WaffleFlagName = keyof typeof waffleFlagDefaults;

View File

@@ -4,6 +4,8 @@ import classNames from 'classnames';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { COURSE_PERMISSIONS } from '@src/authz/constants';
import { useWaffleFlags } from '../../data/apiHooks';
import { otherLinkURLParams } from './constants';
import messages from './messages';
@@ -25,7 +27,7 @@ const HelpSidebar = ({
scheduleAndDetails,
groupConfigurations,
} = otherLinkURLParams;
const waffleFlags = useWaffleFlags();
const waffleFlags = useWaffleFlags(courseId);
const showOtherLink = (params) => !pathname.includes(params);
const generateLegacyURL = (urlParameter) => {
@@ -39,6 +41,26 @@ const HelpSidebar = ({
const advancedSettingsDestination = generateLegacyURL(advancedSettings);
const groupConfigurationsDestination = generateLegacyURL(groupConfigurations);
/*
AuthZ for Course Authoring
If authz.enable_course_authoring flag is enabled, validate permissions using AuthZ API.
*/
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
canManageAdvancedSettings: {
action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
scope: courseId,
},
}, isAuthzEnabled);
// If it's still loading, don't show the Advanced Settings link, otherwise, use the permission to decide
const authzCanManageAdvancedSettings = isLoadingUserPermissions
? false
: !!userPermissions?.canManageAdvancedSettings;
// When authz is enabled, use permission, otherwise it's always allowed (legacy behavior)
const canManageAdvancedSettings = isAuthzEnabled ? authzCanManageAdvancedSettings : true;
return (
<aside className={classNames('help-sidebar', className)}>
<div className="help-sidebar-about">{children}</div>
@@ -90,7 +112,7 @@ const HelpSidebar = ({
isNewPage={waffleFlags.useNewGroupConfigurationsPage}
/>
)}
{showOtherLink(advancedSettings) && (
{showOtherLink(advancedSettings) && canManageAdvancedSettings && (
<HelpSidebarLink
pathToPage={waffleFlags.useNewAdvancedSettingsPage
? `/course/${courseId}/${advancedSettings}` : advancedSettingsDestination}

View File

@@ -1,9 +1,16 @@
// @ts-check
import { waitFor } from '@testing-library/react';
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { initializeMocks, render } from '../../testUtils';
import messages from './messages';
import { HelpSidebar } from '.';
jest.mock('@src/authz/data/apiHooks', () => ({
useUserPermissions: jest.fn(),
}));
const mockPathname = '/foo-bar';
const renderHelpSidebar = (props) => render(
@@ -22,6 +29,11 @@ const props = {
describe('HelpSidebar', () => {
beforeEach(() => {
initializeMocks();
// @ts-ignore
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: undefined,
});
});
it('renders children correctly', () => {
@@ -56,4 +68,41 @@ describe('HelpSidebar', () => {
const { getByText } = renderHelpSidebar(initialProps);
expect(getByText(messages.sidebarLinkToProctoredExamSettings.defaultMessage)).toBeTruthy();
});
it('should render the advanced settings sidebar link when authz.enable_course_authoring flag is enabled and the user is authorized', async () => {
// Mock feature flag
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
// @ts-ignore
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: true },
});
const { queryByText } = renderHelpSidebar(props);
await waitFor(() => expect(queryByText(messages.sidebarLinkToAdvancedSettings.defaultMessage)).toBeTruthy());
});
it('should not render the advanced settings sidebar link when authz.enable_course_authoring flag is enabled and the user is not authorized', async () => {
// Mock feature flag
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
// @ts-ignore
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: false },
});
const { queryByText } = renderHelpSidebar(props);
await waitFor(() => expect(queryByText(messages.sidebarLinkToAdvancedSettings.defaultMessage)).toBeFalsy());
});
it('should not render the advanced settings sidebar link when authz.enable_course_authoring flag is enabled and the permissions are still loading', async () => {
// Mock feature flag
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
// @ts-ignore
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: true,
data: undefined,
});
const { queryByText } = renderHelpSidebar(props);
await waitFor(() => expect(queryByText(messages.sidebarLinkToAdvancedSettings.defaultMessage)).toBeFalsy());
});
});

View File

@@ -1,11 +1,14 @@
import { useSelector } from 'react-redux';
import { getConfig, setConfig } from '@edx/frontend-platform';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode } from 'react';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
import messages from './messages';
import {
useContentMenuItems, useToolsMenuItems, useSettingMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems,
} from './hooks';
import { mockWaffleFlags } from '../data/apiHooks.mock';
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
@@ -27,6 +30,26 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));
jest.mock('@src/authz/data/apiHooks', () => ({
useUserPermissions: jest.fn(),
}));
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
return wrapper;
};
describe('header utils', () => {
describe('getContentMenuItems', () => {
it('when video upload page enabled should include Video Uploads option', () => {
@@ -37,7 +60,7 @@ describe('header utils', () => {
...getConfig(),
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'true',
});
const actualItems = renderHook(() => useContentMenuItems('course-123')).result.current;
const actualItems = renderHook(() => useContentMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
expect(actualItems).toHaveLength(5);
});
it('when video upload page disabled should not include Video Uploads option', () => {
@@ -48,14 +71,14 @@ describe('header utils', () => {
...getConfig(),
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'false',
});
const actualItems = renderHook(() => useContentMenuItems('course-123')).result.current;
const actualItems = renderHook(() => useContentMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
expect(actualItems).toHaveLength(4);
});
it('adds course libraries link to content menu when libraries v2 is enabled', () => {
jest.mocked(useSelector).mockReturnValue({
librariesV2Enabled: true,
});
const actualItems = renderHook(() => useContentMenuItems('course-123')).result.current;
const actualItems = renderHook(() => useContentMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
expect(actualItems[1]).toEqual({ href: '/course/course-123/libraries', title: 'Library Updates' });
});
});
@@ -65,6 +88,10 @@ describe('header utils', () => {
jest.mocked(useSelector).mockReturnValue({
canAccessAdvancedSettings: true,
});
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: true },
} as any);
});
it('when certificate page enabled should include certificates option', () => {
@@ -72,7 +99,7 @@ describe('header utils', () => {
...getConfig(),
ENABLE_CERTIFICATE_PAGE: 'true',
});
const actualItems = renderHook(() => useSettingMenuItems('course-123')).result.current;
const actualItems = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
expect(actualItems).toHaveLength(6);
});
it('when certificate page disabled should not include certificates option', () => {
@@ -80,18 +107,62 @@ describe('header utils', () => {
...getConfig(),
ENABLE_CERTIFICATE_PAGE: 'false',
});
const actualItems = renderHook(() => useSettingMenuItems('course-123')).result.current;
const actualItems = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
expect(actualItems).toHaveLength(5);
});
it('when user has access to advanced settings should include advanced settings option', () => {
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123')).result.current.map((item) => item.title);
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
expect(actualItemsTitle).toContain('Advanced Settings');
});
it('when user has no access to advanced settings should not include advanced settings option', () => {
jest.mocked(useSelector).mockReturnValue({ canAccessAdvancedSettings: false });
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123')).result.current.map((item) => item.title);
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
expect(actualItemsTitle).not.toContain('Advanced Settings');
});
it('when the authz.enable_course_authoring flag is enabled and user has access to advanced settings should include advanced settings option', async () => {
// Mock feature flag
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: true },
} as any);
const { result } = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() });
await waitFor(() => {
const actualItemsTitle = result.current.map((item) => item.title);
expect(actualItemsTitle).toContain('Advanced Settings');
});
});
it('when authz.enable_course_authoring flag is enabled and user has no access to advanced settings should not include advanced settings option', async () => {
// Mock feature flag
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: { canManageAdvancedSettings: false },
} as any);
const { result } = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() });
await waitFor(() => {
const actualItemsTitle = result.current.map((item) => item.title);
expect(actualItemsTitle).not.toContain('Advanced Settings');
});
});
it('when authz.enable_course_authoring flag is enabled and the permission request is still loading, should not include advanced settings option', async () => {
// Mock feature flag
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: true,
data: undefined,
} as any);
const { result } = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() });
await waitFor(() => {
const actualItemsTitle = result.current.map((item) => item.title);
expect(actualItemsTitle).not.toContain('Advanced Settings');
});
});
});
describe('getToolsMenuItems', () => {
@@ -100,7 +171,7 @@ describe('header utils', () => {
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
});
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123')).result.current.map((item) => item.title);
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
expect(actualItemsTitle).toEqual([
'Import',
'Export Course',
@@ -113,7 +184,7 @@ describe('header utils', () => {
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
});
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123')).result.current.map((item) => item.title);
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
expect(actualItemsTitle).toEqual([
'Import',
'Export Course',
@@ -125,7 +196,7 @@ describe('header utils', () => {
mockWaffleFlags({
enableCourseOptimizer: true,
});
const optimizerItem = renderHook(() => useToolsMenuItems('course-123')).result.current.find(
const optimizerItem = renderHook(() => useToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current.find(
item => item.href === '/course/course-123/optimizer',
);
expect(optimizerItem).toBeDefined();
@@ -135,14 +206,14 @@ describe('header utils', () => {
mockWaffleFlags({
enableCourseOptimizer: false,
});
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123')).result.current.map((item) => item.title);
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
expect(actualItemsTitle).not.toContain(messages['header.links.optimizer'].defaultMessage);
});
});
describe('useLibrarySettingsMenuItems', () => {
it('should contain team access url', () => {
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current;
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false), { wrapper: createWrapper() }).result.current;
expect(items).toContainEqual({ title: 'Library Team', href: 'http://localhost/?sa=manage-team' });
});
it('should contain admin console url if set', () => {
@@ -150,7 +221,7 @@ describe('header utils', () => {
...getConfig(),
ADMIN_CONSOLE_URL: 'http://admin-console.com',
});
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current;
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false), { wrapper: createWrapper() }).result.current;
expect(items).toContainEqual({
title: 'Library Team',
href: 'http://admin-console.com/authz/libraries/library-123',
@@ -161,7 +232,7 @@ describe('header utils', () => {
...getConfig(),
ADMIN_CONSOLE_URL: 'http://admin-console.com',
});
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', true)).result.current;
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', true), { wrapper: createWrapper() }).result.current;
expect(items).toContainEqual({
title: 'Library Team',
href: 'http://admin-console.com/authz/libraries/library-123',
@@ -171,7 +242,7 @@ describe('header utils', () => {
describe('useLibraryToolsMenuItems', () => {
it('should contain backup and import url', () => {
const items = renderHook(() => useLibraryToolsMenuItems('course-123')).result.current;
const items = renderHook(() => useLibraryToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
expect(items).toContainEqual({
href: '/library/course-123/backup',
title: 'Back up to local archive',

View File

@@ -9,6 +9,9 @@ import { getStudioHomeData } from '@src/studio-home/data/selectors';
import courseOptimizerMessages from '@src/optimizer-page/messages';
import { SidebarActions } from '@src/library-authoring/common/context/SidebarContext';
import { LibQueryParamKeys } from '@src/library-authoring/routes';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { COURSE_PERMISSIONS } from '@src/authz/constants';
import messages from './messages';
export const useContentMenuItems = (courseId: string) => {
@@ -55,8 +58,29 @@ export const useContentMenuItems = (courseId: string) => {
export const useSettingMenuItems = (courseId: string) => {
const intl = useIntl();
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
const { canAccessAdvancedSettings } = useSelector(getStudioHomeData);
const waffleFlags = useWaffleFlags();
const { canAccessAdvancedSettings: legacyCanAccessAdvancedSettings } = useSelector(getStudioHomeData);
const waffleFlags = useWaffleFlags(courseId);
/*
AuthZ for Course Authoring
If authz.enable_course_authoring flag is enabled, validate permissions using AuthZ API.
Otherwise, fallback to existing logic.
*/
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
canManageAdvancedSettings: {
action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
scope: courseId,
},
}, isAuthzEnabled);
const authzCanManageAdvancedSettings = isLoadingUserPermissions
? false
: userPermissions?.canManageAdvancedSettings || false;
const canAccessAdvancedSettings = isAuthzEnabled
? authzCanManageAdvancedSettings
: legacyCanAccessAdvancedSettings;
const items = [
{
@@ -75,7 +99,7 @@ export const useSettingMenuItems = (courseId: string) => {
href: waffleFlags.useNewGroupConfigurationsPage ? `/course/${courseId}/group_configurations` : `${studioBaseUrl}/group_configurations/${courseId}`,
title: intl.formatMessage(messages['header.links.groupConfigurations']),
},
...(canAccessAdvancedSettings === true
...(canAccessAdvancedSettings
? [{
href: waffleFlags.useNewAdvancedSettingsPage ? `/course/${courseId}/settings/advanced` : `${studioBaseUrl}/settings/advanced/${courseId}`,
title: intl.formatMessage(messages['header.links.advancedSettings']),