From 5ccf39d1302b0a61618d388cda5a7fda8d5d1162 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez <117670175+rodmgwgu@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:39:04 -0600 Subject: [PATCH] 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 --- src/advanced-settings/AdvancedSettings.jsx | 28 ++++- .../AdvancedSettings.test.jsx | 49 +++++++- src/authz/constants.ts | 4 + src/data/api.ts | 1 + src/generic/help-sidebar/HelpSidebar.jsx | 26 ++++- src/generic/help-sidebar/HelpSidebar.test.jsx | 49 ++++++++ src/header/{hooks.test.ts => hooks.test.tsx} | 105 +++++++++++++++--- src/header/hooks.tsx | 30 ++++- 8 files changed, 265 insertions(+), 27 deletions(-) rename src/header/{hooks.test.ts => hooks.test.tsx} (60%) diff --git a/src/advanced-settings/AdvancedSettings.jsx b/src/advanced-settings/AdvancedSettings.jsx index 9ac41f479..553a38023 100644 --- a/src/advanced-settings/AdvancedSettings.jsx +++ b/src/advanced-settings/AdvancedSettings.jsx @@ -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 ; + } + return ( <> @@ -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), }} /> diff --git a/src/advanced-settings/AdvancedSettings.test.jsx b/src/advanced-settings/AdvancedSettings.test.jsx index c66f41193..654f3ca19 100644 --- a/src/advanced-settings/AdvancedSettings.test.jsx +++ b/src/advanced-settings/AdvancedSettings.test.jsx @@ -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) => (