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
28 changes: 25 additions & 3 deletions src/advanced-settings/AdvancedSettings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
Expand All @@ -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),
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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>
Expand Down
49 changes: 47 additions & 2 deletions src/advanced-settings/AdvancedSettings.test.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 />
Expand All @@ -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();
Expand Down Expand Up @@ -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();
});
});
});
4 changes: 4 additions & 0 deletions src/authz/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
1 change: 1 addition & 0 deletions src/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const waffleFlagDefaults = {
useNewGroupConfigurationsPage: true,
useReactMarkdownEditor: true,
useVideoGalleryFlow: false,
enableAuthzCourseAuthoring: false,
} as const;

export type WaffleFlagName = keyof typeof waffleFlagDefaults;
Expand Down
26 changes: 24 additions & 2 deletions src/generic/help-sidebar/HelpSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) => {
Expand All @@ -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;
Comment thread
rodmgwgu marked this conversation as resolved.

return (
<aside className={classNames('help-sidebar', className)}>
<div className="help-sidebar-about">{children}</div>
Expand Down Expand Up @@ -90,7 +112,7 @@ const HelpSidebar = ({
isNewPage={waffleFlags.useNewGroupConfigurationsPage}
/>
)}
{showOtherLink(advancedSettings) && (
{showOtherLink(advancedSettings) && canManageAdvancedSettings && (
<HelpSidebarLink
pathToPage={waffleFlags.useNewAdvancedSettingsPage
? `/course/${courseId}/${advancedSettings}` : advancedSettingsDestination}
Expand Down
49 changes: 49 additions & 0 deletions src/generic/help-sidebar/HelpSidebar.test.jsx
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -22,6 +29,11 @@ const props = {
describe('HelpSidebar', () => {
beforeEach(() => {
initializeMocks();
// @ts-ignore
jest.mocked(useUserPermissions).mockReturnValue({
isLoading: false,
data: undefined,
});
});

it('renders children correctly', () => {
Expand Down Expand Up @@ -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());
});
});
Loading