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) => (