From f57976e6c9f4112c90219a3cfcb10e81d27a6c91 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Thu, 20 Nov 2025 10:49:18 -0600 Subject: [PATCH 01/13] feat: Implement querying openedx-authz for publish permissions --- src/authz/constants.ts | 18 ++++++++++ src/authz/data/api.ts | 10 ++++++ src/authz/data/hooks.ts | 34 +++++++++++++++++++ src/authz/data/utils.ts | 4 +++ src/authz/types.ts | 8 +++++ .../common/context/LibraryContext.tsx | 17 +++++++++- .../library-info/LibraryPublishStatus.tsx | 6 ++-- 7 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/authz/constants.ts create mode 100644 src/authz/data/api.ts create mode 100644 src/authz/data/hooks.ts create mode 100644 src/authz/data/utils.ts create mode 100644 src/authz/types.ts diff --git a/src/authz/constants.ts b/src/authz/constants.ts new file mode 100644 index 0000000000..ab386ce49e --- /dev/null +++ b/src/authz/constants.ts @@ -0,0 +1,18 @@ +export const appId = 'org.openedx.frontend.app.adminConsole'; + +export const CONTENT_LIBRARY_PERMISSIONS = { + DELETE_LIBRARY: 'content_libraries.delete_library', + MANAGE_LIBRARY_TAGS: 'content_libraries.manage_library_tags', + VIEW_LIBRARY: 'content_libraries.view_library', + + EDIT_LIBRARY_CONTENT: 'content_libraries.edit_library_content', + PUBLISH_LIBRARY_CONTENT: 'content_libraries.publish_library_content', + REUSE_LIBRARY_CONTENT: 'content_libraries.reuse_library_content', + + CREATE_LIBRARY_COLLECTION: 'content_libraries.create_library_collection', + EDIT_LIBRARY_COLLECTION: 'content_libraries.edit_library_collection', + DELETE_LIBRARY_COLLECTION: 'content_libraries.delete_library_collection', + + MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team', + VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team', +}; \ No newline at end of file diff --git a/src/authz/data/api.ts b/src/authz/data/api.ts new file mode 100644 index 0000000000..5c171c7764 --- /dev/null +++ b/src/authz/data/api.ts @@ -0,0 +1,10 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { PermissionValidationRequest, PermissionValidationResponse } from '@src/authz/types'; +import { getApiUrl } from './utils'; + +export const validateUserPermissions = async ( + validations: PermissionValidationRequest[], +): Promise => { + const { data } = await getAuthenticatedHttpClient().post(getApiUrl('/api/authz/v1/permissions/validate/me'), validations); + return data; +}; \ No newline at end of file diff --git a/src/authz/data/hooks.ts b/src/authz/data/hooks.ts new file mode 100644 index 0000000000..96bc078f8b --- /dev/null +++ b/src/authz/data/hooks.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { PermissionValidationRequest, PermissionValidationResponse } from '@src/authz/types'; +import { appId } from '@src/authz/constants'; +import { validateUserPermissions } from './api'; + +const adminConsoleQueryKeys = { + all: [appId] as const, + permissions: (permissions: PermissionValidationRequest[]) => [...adminConsoleQueryKeys.all, 'validatePermissions', permissions] as const, +}; + +/** + * React Query hook to validate if the current user has permissions over a certain object in the instance. + * It helps to: + * - Determine whether the current user can access certain object. + * - Provide role-based rendering logic for UI components. + * + * @param permissions - The array of objects and actions to validate. + * + * @example + * const { data } = useValidateUserPermissions([{ + "action": "act:read", + "object": "lib:test-lib", + "scope": "org:OpenedX" + }]); + * if (data[0].allowed) { ... } + * + */ +export const useValidateUserPermissions = ( + permissions: PermissionValidationRequest[], +) => useQuery({ + queryKey: adminConsoleQueryKeys.permissions(permissions), + queryFn: () => validateUserPermissions(permissions), + retry: false, +}); \ No newline at end of file diff --git a/src/authz/data/utils.ts b/src/authz/data/utils.ts new file mode 100644 index 0000000000..892208b512 --- /dev/null +++ b/src/authz/data/utils.ts @@ -0,0 +1,4 @@ +import { getConfig } from '@edx/frontend-platform'; + +export const getApiUrl = (path: string) => `${getConfig().LMS_BASE_URL}${path || ''}`; +export const getStudioApiUrl = (path: string) => `${getConfig().STUDIO_BASE_URL}${path || ''}`; \ No newline at end of file diff --git a/src/authz/types.ts b/src/authz/types.ts new file mode 100644 index 0000000000..1af21f63b9 --- /dev/null +++ b/src/authz/types.ts @@ -0,0 +1,8 @@ +export interface PermissionValidationRequest { + action: string; + scope?: string; +} + +export interface PermissionValidationResponse extends PermissionValidationRequest { + allowed: boolean; +} \ No newline at end of file diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index 13a38472fd..ed1c88c9b2 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -13,6 +13,12 @@ import type { ComponentPicker } from '../../component-picker'; import type { ContentLibrary, BlockTypeMetadata } from '../../data/api'; import { useContentLibrary } from '../../data/apiHooks'; import { useComponentPickerContext } from './ComponentPickerContext'; +import { useValidateUserPermissions } from '@src/authz/data/hooks'; +import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants'; + +const LIBRARY_PERMISSIONS = [ + CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, +]; export interface ComponentEditorInfo { usageKey: string; @@ -25,6 +31,7 @@ export type LibraryContextData = { libraryId: string; libraryData?: ContentLibrary; readOnly: boolean; + canPublish: boolean; isLoadingLibraryData: boolean; /** The ID of the current collection/container, on the sidebar OR page */ collectionId: string | undefined; @@ -107,6 +114,11 @@ export const LibraryProvider = ({ componentPickerMode, } = useComponentPickerContext(); + const permissions = LIBRARY_PERMISSIONS.map(action => ({ action, scope: libraryId })); + + const { isLoading: isLoadingUserPermissions, data: userPermissions } = useValidateUserPermissions(permissions); + const canPublish = userPermissions ? userPermissions[0]?.allowed : false; + // TODO change to use canEdit from userPermissions later const readOnly = !!componentPickerMode || !libraryData?.canEditLibrary; // Parse the initial collectionId and/or container ID(s) from the current URL params @@ -131,7 +143,8 @@ export const LibraryProvider = ({ containerId, setContainerId, readOnly, - isLoadingLibraryData, + canPublish, + isLoadingLibraryData: isLoadingLibraryData || isLoadingUserPermissions, showOnlyPublished, extraFilter, isCreateCollectionModalOpen, @@ -154,7 +167,9 @@ export const LibraryProvider = ({ containerId, setContainerId, readOnly, + canPublish, isLoadingLibraryData, + isLoadingUserPermissions, showOnlyPublished, extraFilter, isCreateCollectionModalOpen, diff --git a/src/library-authoring/library-info/LibraryPublishStatus.tsx b/src/library-authoring/library-info/LibraryPublishStatus.tsx index b046b858a6..c22d04f9ed 100644 --- a/src/library-authoring/library-info/LibraryPublishStatus.tsx +++ b/src/library-authoring/library-info/LibraryPublishStatus.tsx @@ -12,7 +12,7 @@ import messages from './messages'; const LibraryPublishStatus = () => { const intl = useIntl(); - const { libraryData, readOnly } = useLibraryContext(); + const { libraryData, readOnly, canPublish } = useLibraryContext(); const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false); const commitLibraryChanges = useCommitLibraryChanges(); @@ -51,10 +51,10 @@ const LibraryPublishStatus = () => { <> Date: Thu, 20 Nov 2025 12:59:02 -0600 Subject: [PATCH 02/13] squash!: Fix lint issues, correct appId --- src/authz/constants.ts | 4 ++-- src/authz/data/api.ts | 2 +- src/authz/data/hooks.ts | 2 +- src/authz/data/utils.ts | 2 +- src/authz/types.ts | 2 +- src/library-authoring/common/context/LibraryContext.tsx | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/authz/constants.ts b/src/authz/constants.ts index ab386ce49e..4e21520430 100644 --- a/src/authz/constants.ts +++ b/src/authz/constants.ts @@ -1,4 +1,4 @@ -export const appId = 'org.openedx.frontend.app.adminConsole'; +export const appId = 'org.openedx.frontend.authoring'; export const CONTENT_LIBRARY_PERMISSIONS = { DELETE_LIBRARY: 'content_libraries.delete_library', @@ -15,4 +15,4 @@ export const CONTENT_LIBRARY_PERMISSIONS = { MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team', VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team', -}; \ No newline at end of file +}; diff --git a/src/authz/data/api.ts b/src/authz/data/api.ts index 5c171c7764..218689f71c 100644 --- a/src/authz/data/api.ts +++ b/src/authz/data/api.ts @@ -7,4 +7,4 @@ export const validateUserPermissions = async ( ): Promise => { const { data } = await getAuthenticatedHttpClient().post(getApiUrl('/api/authz/v1/permissions/validate/me'), validations); return data; -}; \ No newline at end of file +}; diff --git a/src/authz/data/hooks.ts b/src/authz/data/hooks.ts index 96bc078f8b..1a4c6756ef 100644 --- a/src/authz/data/hooks.ts +++ b/src/authz/data/hooks.ts @@ -31,4 +31,4 @@ export const useValidateUserPermissions = ( queryKey: adminConsoleQueryKeys.permissions(permissions), queryFn: () => validateUserPermissions(permissions), retry: false, -}); \ No newline at end of file +}); diff --git a/src/authz/data/utils.ts b/src/authz/data/utils.ts index 892208b512..8676ba1abd 100644 --- a/src/authz/data/utils.ts +++ b/src/authz/data/utils.ts @@ -1,4 +1,4 @@ import { getConfig } from '@edx/frontend-platform'; export const getApiUrl = (path: string) => `${getConfig().LMS_BASE_URL}${path || ''}`; -export const getStudioApiUrl = (path: string) => `${getConfig().STUDIO_BASE_URL}${path || ''}`; \ No newline at end of file +export const getStudioApiUrl = (path: string) => `${getConfig().STUDIO_BASE_URL}${path || ''}`; diff --git a/src/authz/types.ts b/src/authz/types.ts index 1af21f63b9..902df408f4 100644 --- a/src/authz/types.ts +++ b/src/authz/types.ts @@ -5,4 +5,4 @@ export interface PermissionValidationRequest { export interface PermissionValidationResponse extends PermissionValidationRequest { allowed: boolean; -} \ No newline at end of file +} diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index ed1c88c9b2..805b2f5772 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -7,14 +7,14 @@ import { useState, } from 'react'; import { useParams } from 'react-router-dom'; +import { useValidateUserPermissions } from '@src/authz/data/hooks'; +import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants'; import { ContainerType } from '../../../generic/key-utils'; import type { ComponentPicker } from '../../component-picker'; import type { ContentLibrary, BlockTypeMetadata } from '../../data/api'; import { useContentLibrary } from '../../data/apiHooks'; import { useComponentPickerContext } from './ComponentPickerContext'; -import { useValidateUserPermissions } from '@src/authz/data/hooks'; -import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants'; const LIBRARY_PERMISSIONS = [ CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, From 5f64fb3c693019c7a3a8af60db91f33449699eee Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Fri, 21 Nov 2025 11:08:33 -0600 Subject: [PATCH 03/13] squash!: Fixing permissions --- .../create-container/CreateContainerModal.test.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/library-authoring/create-container/CreateContainerModal.test.tsx b/src/library-authoring/create-container/CreateContainerModal.test.tsx index a6523bf124..9c8bf8ad9f 100644 --- a/src/library-authoring/create-container/CreateContainerModal.test.tsx +++ b/src/library-authoring/create-container/CreateContainerModal.test.tsx @@ -80,10 +80,12 @@ describe('CreateContainerModal container linking', () => { const createButton = await screen.findByRole('button', { name: /create/i }); await user.click(createButton); await waitFor(() => { - expect(axiosMock.history.post).toHaveLength(1); + const request = axiosMock.history.post.find(req => req.url.match(/\/api\/libraries\/.*\/containers/)); + expect(request).toBeDefined(); }); - expect(axiosMock.history.post[0].url).toMatch(/\/api\/libraries\/.*\/containers/); - expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({ + const request = axiosMock.history.post.find(req => req.url.match(/\/api\/libraries\/.*\/containers/)); + expect(request.url).toMatch(/\/api\/libraries\/.*\/containers/); + expect(JSON.parse(request.data)).toEqual({ can_stand_alone: true, container_type: 'section', display_name: 'Test Section', @@ -114,9 +116,11 @@ describe('CreateContainerModal container linking', () => { const createButton = await screen.findByRole('button', { name: /create/i }); await user.click(createButton); await waitFor(() => { - expect(axiosMock.history.post[0].url).toMatch(/\/api\/libraries\/.*\/containers/); + const request = axiosMock.history.post.find(req => req.url.match(/\/api\/libraries\/.*\/containers/)); + expect(request).toBeDefined(); }); - expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({ + const request = axiosMock.history.post.find(req => req.url.match(/\/api\/libraries\/.*\/containers/)); + expect(JSON.parse(request.data)).toEqual({ can_stand_alone: false, container_type: 'subsection', display_name: 'Test Subsection', From d7b1a3c413b167b992d7c5a73229e57ed1b61cb4 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Mon, 24 Nov 2025 14:47:54 -0600 Subject: [PATCH 04/13] squash!: Fix existing tests --- src/testUtils.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/testUtils.tsx b/src/testUtils.tsx index 7270d3c0c9..8461776de7 100644 --- a/src/testUtils.tsx +++ b/src/testUtils.tsx @@ -26,6 +26,9 @@ import { import { ToastContext, type ToastContextData } from './generic/toast-context'; import initializeReduxStore, { type DeprecatedReduxState } from './store'; import { getApiWaffleFlagsUrl } from './data/api'; +import { CONTENT_LIBRARY_PERMISSIONS } from './authz/constants'; +import * as authzApi from '@src/authz/data/api'; + /** @deprecated Use React Query and/or regular React Context instead of redux */ let reduxStore: Store; @@ -192,6 +195,14 @@ export function initializeMocks({ user = defaultUser, initialState = undefined } // Clear the call counts etc. of all mocks. This doesn't remove the mock's effects; just clears their history. jest.clearAllMocks(); + // Mock user permissions to avoid breaking tests that monitor axios calls + jest.spyOn(authzApi, 'validateUserPermissions').mockResolvedValue([ + { + action: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, + allowed: true, + }, + ]); + return { reduxStore, axiosMock, From e5f10d2038058ddab8b7761e89cf722595d2d937 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Mon, 24 Nov 2025 14:52:27 -0600 Subject: [PATCH 05/13] squash!: Fix lint issues --- src/testUtils.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/testUtils.tsx b/src/testUtils.tsx index 8461776de7..9ec72ecba0 100644 --- a/src/testUtils.tsx +++ b/src/testUtils.tsx @@ -23,12 +23,11 @@ import { Routes, } from 'react-router-dom'; +import * as authzApi from '@src/authz/data/api'; import { ToastContext, type ToastContextData } from './generic/toast-context'; import initializeReduxStore, { type DeprecatedReduxState } from './store'; import { getApiWaffleFlagsUrl } from './data/api'; import { CONTENT_LIBRARY_PERMISSIONS } from './authz/constants'; -import * as authzApi from '@src/authz/data/api'; - /** @deprecated Use React Query and/or regular React Context instead of redux */ let reduxStore: Store; From 03a8d7dcccb13c482bf18ef20a90b6e838888b78 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Mon, 24 Nov 2025 15:53:50 -0600 Subject: [PATCH 06/13] squash!: Improve way of mocking validateUserPermissions, remove unneeded changes --- .../CreateContainerModal.test.tsx | 16 ++++++---------- .../library-info/LibraryInfo.test.tsx | 11 +++++++++++ src/testUtils.tsx | 11 ++++------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/library-authoring/create-container/CreateContainerModal.test.tsx b/src/library-authoring/create-container/CreateContainerModal.test.tsx index 9c8bf8ad9f..41e6c23812 100644 --- a/src/library-authoring/create-container/CreateContainerModal.test.tsx +++ b/src/library-authoring/create-container/CreateContainerModal.test.tsx @@ -80,12 +80,10 @@ describe('CreateContainerModal container linking', () => { const createButton = await screen.findByRole('button', { name: /create/i }); await user.click(createButton); await waitFor(() => { - const request = axiosMock.history.post.find(req => req.url.match(/\/api\/libraries\/.*\/containers/)); - expect(request).toBeDefined(); + expect(axiosMock.history.post).toHaveLength(1); }); - const request = axiosMock.history.post.find(req => req.url.match(/\/api\/libraries\/.*\/containers/)); - expect(request.url).toMatch(/\/api\/libraries\/.*\/containers/); - expect(JSON.parse(request.data)).toEqual({ + expect(axiosMock.history.post[0].url).toMatch(/\/api\/libraries\/.*\/containers/); + expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({ can_stand_alone: true, container_type: 'section', display_name: 'Test Section', @@ -116,11 +114,9 @@ describe('CreateContainerModal container linking', () => { const createButton = await screen.findByRole('button', { name: /create/i }); await user.click(createButton); await waitFor(() => { - const request = axiosMock.history.post.find(req => req.url.match(/\/api\/libraries\/.*\/containers/)); - expect(request).toBeDefined(); + expect(axiosMock.history.post[0].url).toMatch(/\/api\/libraries\/.*\/containers/); }); - const request = axiosMock.history.post.find(req => req.url.match(/\/api\/libraries\/.*\/containers/)); - expect(JSON.parse(request.data)).toEqual({ + expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({ can_stand_alone: false, container_type: 'subsection', display_name: 'Test Subsection', @@ -148,4 +144,4 @@ describe('CreateContainerModal container linking', () => { expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(/error/i)); }); }); -}); +}); \ No newline at end of file diff --git a/src/library-authoring/library-info/LibraryInfo.test.tsx b/src/library-authoring/library-info/LibraryInfo.test.tsx index 0971d5d051..abbbeaaa3b 100644 --- a/src/library-authoring/library-info/LibraryInfo.test.tsx +++ b/src/library-authoring/library-info/LibraryInfo.test.tsx @@ -8,6 +8,8 @@ import { waitFor, initializeMocks, } from '@src/testUtils'; +import { validateUserPermissions } from '@src/authz/data/api'; +import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants'; import { mockContentLibrary } from '../data/api.mocks'; import { getCommitLibraryChangesUrl } from '../data/api'; import { LibraryProvider } from '../common/context/LibraryContext'; @@ -33,6 +35,7 @@ const render = (libraryId: string = mockLibraryId) => baseRender( let axiosMock: MockAdapter; let mockShowToast: (message: string) => void; +let validateUserPermissionsMock: jest.SpiedFunction; mockContentLibrary.applyMock(); @@ -41,6 +44,14 @@ describe('', () => { const mocks = initializeMocks(); axiosMock = mocks.axiosMock; mockShowToast = mocks.mockShowToast; + validateUserPermissionsMock = mocks.validateUserPermissionsMock; + + validateUserPermissionsMock.mockResolvedValue([ + { + action: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, + allowed: true, + }, + ]); }); afterEach(() => { diff --git a/src/testUtils.tsx b/src/testUtils.tsx index 9ec72ecba0..cb75904e43 100644 --- a/src/testUtils.tsx +++ b/src/testUtils.tsx @@ -27,12 +27,12 @@ import * as authzApi from '@src/authz/data/api'; import { ToastContext, type ToastContextData } from './generic/toast-context'; import initializeReduxStore, { type DeprecatedReduxState } from './store'; import { getApiWaffleFlagsUrl } from './data/api'; -import { CONTENT_LIBRARY_PERMISSIONS } from './authz/constants'; /** @deprecated Use React Query and/or regular React Context instead of redux */ let reduxStore: Store; let queryClient: QueryClient; let axiosMock: MockAdapter; +let validateUserPermissionsMock: jest.SpiedFunction; /** To use this: `const { mockShowToast } = initializeMocks()` and `expect(mockShowToast).toHaveBeenCalled()` */ let mockToastContext: ToastContextData = { @@ -195,12 +195,8 @@ export function initializeMocks({ user = defaultUser, initialState = undefined } jest.clearAllMocks(); // Mock user permissions to avoid breaking tests that monitor axios calls - jest.spyOn(authzApi, 'validateUserPermissions').mockResolvedValue([ - { - action: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, - allowed: true, - }, - ]); + // If needed, override the mockResolvedValue in your test + validateUserPermissionsMock = jest.spyOn(authzApi, 'validateUserPermissions').mockResolvedValue([]); return { reduxStore, @@ -208,6 +204,7 @@ export function initializeMocks({ user = defaultUser, initialState = undefined } mockShowToast: mockToastContext.showToast, mockToastAction: mockToastContext.toastAction, queryClient, + validateUserPermissionsMock, }; } From b21a5203ee7bd856cbb2439834430259b5bb1d2d Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Mon, 24 Nov 2025 15:55:21 -0600 Subject: [PATCH 07/13] squash!: lint fix --- .../create-container/CreateContainerModal.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/library-authoring/create-container/CreateContainerModal.test.tsx b/src/library-authoring/create-container/CreateContainerModal.test.tsx index 41e6c23812..a6523bf124 100644 --- a/src/library-authoring/create-container/CreateContainerModal.test.tsx +++ b/src/library-authoring/create-container/CreateContainerModal.test.tsx @@ -144,4 +144,4 @@ describe('CreateContainerModal container linking', () => { expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(/error/i)); }); }); -}); \ No newline at end of file +}); From 35d53abca19a74598ba59386c9667f5e9e770572 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Mon, 24 Nov 2025 16:19:00 -0600 Subject: [PATCH 08/13] squash!: Add test for authz hook --- src/authz/data/hooks.test.tsx | 98 +++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/authz/data/hooks.test.tsx diff --git a/src/authz/data/hooks.test.tsx b/src/authz/data/hooks.test.tsx new file mode 100644 index 0000000000..209e98b8a3 --- /dev/null +++ b/src/authz/data/hooks.test.tsx @@ -0,0 +1,98 @@ +import { act, ReactNode } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { useValidateUserPermissions } from './hooks'; + +jest.mock('@edx/frontend-platform/auth', () => ({ + getAuthenticatedHttpClient: jest.fn(), +})); + +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + + return wrapper; +}; + +const permissions = [ + { + action: 'act:read', + object: 'lib:test-lib', + scope: 'org:OpenedX', + }, +]; + +const mockValidPermissions = [ + { action: 'act:read', object: 'lib:test-lib', allowed: true }, +]; + +const mockInvalidPermissions = [ + { action: 'act:read', object: 'lib:test-lib', allowed: false }, +]; + +describe('useValidateUserPermissions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns allowed true when permissions are valid', async () => { + getAuthenticatedHttpClient.mockReturnValue({ + post: jest.fn().mockResolvedValueOnce({ data: mockValidPermissions }), + }); + + const { result } = renderHook(() => useValidateUserPermissions(permissions), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current).toBeDefined()); + await waitFor(() => expect(result.current.data).toBeDefined()); + + expect(getAuthenticatedHttpClient).toHaveBeenCalled(); + expect(result.current.data![0].allowed).toBe(true); + }); + + it('returns allowed false when permissions are invalid', async () => { + getAuthenticatedHttpClient.mockReturnValue({ + post: jest.fn().mockResolvedValue({ data: mockInvalidPermissions }), + }); + + const { result } = renderHook(() => useValidateUserPermissions(permissions), { + wrapper: createWrapper(), + }); + await waitFor(() => expect(result.current).toBeDefined()); + await waitFor(() => expect(result.current.data).toBeDefined()); + + expect(getAuthenticatedHttpClient).toHaveBeenCalled(); + expect(result.current.data![0].allowed).toBe(false); + }); + + it('handles error when the API call fails', async () => { + const mockError = new Error('API Error'); + + getAuthenticatedHttpClient.mockReturnValue({ + post: jest.fn().mockRejectedValue(new Error('API Error')), + }); + + try { + act(() => { + renderHook(() => useValidateUserPermissions(permissions), { + wrapper: createWrapper(), + }); + }); + } catch (error) { + expect(error).toEqual(mockError); // Check for the expected error + } + }); +}); From f7c566ff910d24e84d949834ffd0b6dccd064055 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Tue, 25 Nov 2025 16:59:17 -0600 Subject: [PATCH 09/13] squash!: Attend PR comments --- src/authz/data/{hooks.test.tsx => apiHooks.test.tsx} | 2 +- src/authz/data/{hooks.ts => apiHooks.ts} | 0 src/library-authoring/common/context/LibraryContext.tsx | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) rename src/authz/data/{hooks.test.tsx => apiHooks.test.tsx} (97%) rename src/authz/data/{hooks.ts => apiHooks.ts} (100%) diff --git a/src/authz/data/hooks.test.tsx b/src/authz/data/apiHooks.test.tsx similarity index 97% rename from src/authz/data/hooks.test.tsx rename to src/authz/data/apiHooks.test.tsx index 209e98b8a3..5536bd4aaf 100644 --- a/src/authz/data/hooks.test.tsx +++ b/src/authz/data/apiHooks.test.tsx @@ -2,7 +2,7 @@ import { act, ReactNode } from 'react'; import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { useValidateUserPermissions } from './hooks'; +import { useValidateUserPermissions } from './apiHooks'; jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedHttpClient: jest.fn(), diff --git a/src/authz/data/hooks.ts b/src/authz/data/apiHooks.ts similarity index 100% rename from src/authz/data/hooks.ts rename to src/authz/data/apiHooks.ts diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index 805b2f5772..d948921ad3 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -7,7 +7,7 @@ import { useState, } from 'react'; import { useParams } from 'react-router-dom'; -import { useValidateUserPermissions } from '@src/authz/data/hooks'; +import { useValidateUserPermissions } from '@src/authz/data/apiHooks'; import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants'; import { ContainerType } from '../../../generic/key-utils'; @@ -118,7 +118,6 @@ export const LibraryProvider = ({ const { isLoading: isLoadingUserPermissions, data: userPermissions } = useValidateUserPermissions(permissions); const canPublish = userPermissions ? userPermissions[0]?.allowed : false; - // TODO change to use canEdit from userPermissions later const readOnly = !!componentPickerMode || !libraryData?.canEditLibrary; // Parse the initial collectionId and/or container ID(s) from the current URL params From 129e7710dab50c85e3a1e4a18e3800474f8eca29 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Fri, 28 Nov 2025 13:13:23 -0600 Subject: [PATCH 10/13] squash!: use authz instead of appId for useValidateUserPermissions query --- src/authz/constants.ts | 2 -- src/authz/data/apiHooks.ts | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/authz/constants.ts b/src/authz/constants.ts index 4e21520430..b9b14bbbf0 100644 --- a/src/authz/constants.ts +++ b/src/authz/constants.ts @@ -1,5 +1,3 @@ -export const appId = 'org.openedx.frontend.authoring'; - export const CONTENT_LIBRARY_PERMISSIONS = { DELETE_LIBRARY: 'content_libraries.delete_library', MANAGE_LIBRARY_TAGS: 'content_libraries.manage_library_tags', diff --git a/src/authz/data/apiHooks.ts b/src/authz/data/apiHooks.ts index 1a4c6756ef..d4dbb32286 100644 --- a/src/authz/data/apiHooks.ts +++ b/src/authz/data/apiHooks.ts @@ -1,10 +1,9 @@ import { useQuery } from '@tanstack/react-query'; import { PermissionValidationRequest, PermissionValidationResponse } from '@src/authz/types'; -import { appId } from '@src/authz/constants'; import { validateUserPermissions } from './api'; const adminConsoleQueryKeys = { - all: [appId] as const, + all: ['authz'], permissions: (permissions: PermissionValidationRequest[]) => [...adminConsoleQueryKeys.all, 'validatePermissions', permissions] as const, }; @@ -19,7 +18,6 @@ const adminConsoleQueryKeys = { * @example * const { data } = useValidateUserPermissions([{ "action": "act:read", - "object": "lib:test-lib", "scope": "org:OpenedX" }]); * if (data[0].allowed) { ... } From a435e1da6bd37dce8fd9a3a7a6e69c7ad75b9620 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Mon, 1 Dec 2025 12:05:45 -0600 Subject: [PATCH 11/13] squash!: Improve permission validation hook with a clearer and safer API --- src/authz/data/api.ts | 41 ++++++- src/authz/data/apiHooks.test.tsx | 114 ++++++++++++++---- src/authz/data/apiHooks.ts | 26 ++-- src/authz/types.ts | 12 +- .../common/context/LibraryContext.tsx | 17 ++- 5 files changed, 161 insertions(+), 49 deletions(-) diff --git a/src/authz/data/api.ts b/src/authz/data/api.ts index 218689f71c..9801f8be88 100644 --- a/src/authz/data/api.ts +++ b/src/authz/data/api.ts @@ -1,10 +1,41 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { PermissionValidationRequest, PermissionValidationResponse } from '@src/authz/types'; +import { + PermissionValidationAnswer, + PermissionValidationQuery, + PermissionValidationRequestItem, + PermissionValidationResponseItem, +} from '@src/authz/types'; import { getApiUrl } from './utils'; export const validateUserPermissions = async ( - validations: PermissionValidationRequest[], -): Promise => { - const { data } = await getAuthenticatedHttpClient().post(getApiUrl('/api/authz/v1/permissions/validate/me'), validations); - return data; + query: PermissionValidationQuery, +): Promise => { + // Convert the validations query object into an array for the API request + const request: PermissionValidationRequestItem[] = Object.values(query); + + const { data }: { data: PermissionValidationResponseItem[] } = await getAuthenticatedHttpClient().post( + getApiUrl('/api/authz/v1/permissions/validate/me'), + request, + ); + + // Convert the API response back into the expected answer format + const result: PermissionValidationAnswer = {}; + data.forEach((item: { action: string; scope?: string; allowed: boolean }) => { + const key = Object.keys(query).find( + (k) => query[k].action === item.action + && query[k].scope === item.scope, + ); + if (key) { + result[key] = item.allowed; + } + }); + + // Fill any missing keys with false + Object.keys(query).forEach((key) => { + if (!(key in result)) { + result[key] = false; + } + }); + + return result; }; diff --git a/src/authz/data/apiHooks.test.tsx b/src/authz/data/apiHooks.test.tsx index 5536bd4aaf..e6bf311f1c 100644 --- a/src/authz/data/apiHooks.test.tsx +++ b/src/authz/data/apiHooks.test.tsx @@ -2,7 +2,7 @@ import { act, ReactNode } from 'react'; import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { useValidateUserPermissions } from './apiHooks'; +import { useUserPermissions } from './apiHooks'; jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedHttpClient: jest.fn(), @@ -18,41 +18,63 @@ const createWrapper = () => { }); const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - + {children} ); return wrapper; }; -const permissions = [ - { +const singlePermission = { + canRead: { action: 'act:read', - object: 'lib:test-lib', - scope: 'org:OpenedX', + scope: 'lib:test-lib', }, +}; + +const mockValidSinglePermission = [ + { action: 'act:read', scope: 'lib:test-lib', allowed: true }, +]; + +const mockInvalidSinglePermission = [ + { action: 'act:read', scope: 'lib:test-lib', allowed: false }, +]; + +const mockEmptyPermissions = [ + // No permissions returned ]; -const mockValidPermissions = [ - { action: 'act:read', object: 'lib:test-lib', allowed: true }, +const multiplePermissions = { + canRead: { + action: 'act:read', + scope: 'lib:test-lib', + }, + canWrite: { + action: 'act:write', + scope: 'lib:test-lib', + }, +}; + +const mockValidMultiplePermissions = [ + { action: 'act:read', scope: 'lib:test-lib', allowed: true }, + { action: 'act:write', scope: 'lib:test-lib', allowed: true }, ]; -const mockInvalidPermissions = [ - { action: 'act:read', object: 'lib:test-lib', allowed: false }, +const mockInvalidMultiplePermissions = [ + { action: 'act:read', scope: 'lib:test-lib', allowed: false }, + { action: 'act:write', scope: 'lib:test-lib', allowed: false }, ]; -describe('useValidateUserPermissions', () => { +describe('useUserPermissions', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('returns allowed true when permissions are valid', async () => { + it('returns allowed true when permission is valid', async () => { getAuthenticatedHttpClient.mockReturnValue({ - post: jest.fn().mockResolvedValueOnce({ data: mockValidPermissions }), + post: jest.fn().mockResolvedValueOnce({ data: mockValidSinglePermission }), }); - const { result } = renderHook(() => useValidateUserPermissions(permissions), { + const { result } = renderHook(() => useUserPermissions(singlePermission), { wrapper: createWrapper(), }); @@ -60,22 +82,70 @@ describe('useValidateUserPermissions', () => { await waitFor(() => expect(result.current.data).toBeDefined()); expect(getAuthenticatedHttpClient).toHaveBeenCalled(); - expect(result.current.data![0].allowed).toBe(true); + expect(result.current.data!.canRead).toBe(true); + }); + + it('returns allowed false when permission is invalid', async () => { + getAuthenticatedHttpClient.mockReturnValue({ + post: jest.fn().mockResolvedValue({ data: mockInvalidSinglePermission }), + }); + + const { result } = renderHook(() => useUserPermissions(singlePermission), { + wrapper: createWrapper(), + }); + await waitFor(() => expect(result.current).toBeDefined()); + await waitFor(() => expect(result.current.data).toBeDefined()); + + expect(getAuthenticatedHttpClient).toHaveBeenCalled(); + expect(result.current.data!.canRead).toBe(false); + }); + + it('returns allowed true when multiple permissions are valid', async () => { + getAuthenticatedHttpClient.mockReturnValue({ + post: jest.fn().mockResolvedValueOnce({ data: mockValidMultiplePermissions }), + }); + + const { result } = renderHook(() => useUserPermissions(multiplePermissions), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current).toBeDefined()); + await waitFor(() => expect(result.current.data).toBeDefined()); + + expect(getAuthenticatedHttpClient).toHaveBeenCalled(); + expect(result.current.data!.canRead).toBe(true); + expect(result.current.data!.canWrite).toBe(true); + }); + + it('returns allowed false when multiple permissions are invalid', async () => { + getAuthenticatedHttpClient.mockReturnValue({ + post: jest.fn().mockResolvedValue({ data: mockInvalidMultiplePermissions }), + }); + + const { result } = renderHook(() => useUserPermissions(multiplePermissions), { + wrapper: createWrapper(), + }); + await waitFor(() => expect(result.current).toBeDefined()); + await waitFor(() => expect(result.current.data).toBeDefined()); + + expect(getAuthenticatedHttpClient).toHaveBeenCalled(); + expect(result.current.data!.canRead).toBe(false); + expect(result.current.data!.canWrite).toBe(false); }); - it('returns allowed false when permissions are invalid', async () => { + it('returns allowed false when the permission is not included in the server response', async () => { getAuthenticatedHttpClient.mockReturnValue({ - post: jest.fn().mockResolvedValue({ data: mockInvalidPermissions }), + post: jest.fn().mockResolvedValue({ data: mockEmptyPermissions }), }); - const { result } = renderHook(() => useValidateUserPermissions(permissions), { + const { result } = renderHook(() => useUserPermissions(singlePermission), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current).toBeDefined()); await waitFor(() => expect(result.current.data).toBeDefined()); expect(getAuthenticatedHttpClient).toHaveBeenCalled(); - expect(result.current.data![0].allowed).toBe(false); + expect(result.current.data!.canRead).toBe(false); }); it('handles error when the API call fails', async () => { @@ -87,7 +157,7 @@ describe('useValidateUserPermissions', () => { try { act(() => { - renderHook(() => useValidateUserPermissions(permissions), { + renderHook(() => useUserPermissions(singlePermission), { wrapper: createWrapper(), }); }); diff --git a/src/authz/data/apiHooks.ts b/src/authz/data/apiHooks.ts index d4dbb32286..2384c72591 100644 --- a/src/authz/data/apiHooks.ts +++ b/src/authz/data/apiHooks.ts @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query'; -import { PermissionValidationRequest, PermissionValidationResponse } from '@src/authz/types'; +import { PermissionValidationAnswer, PermissionValidationQuery } from '@src/authz/types'; import { validateUserPermissions } from './api'; const adminConsoleQueryKeys = { all: ['authz'], - permissions: (permissions: PermissionValidationRequest[]) => [...adminConsoleQueryKeys.all, 'validatePermissions', permissions] as const, + permissions: (permissions: PermissionValidationQuery) => [...adminConsoleQueryKeys.all, 'validatePermissions', permissions] as const, }; /** @@ -13,19 +13,23 @@ const adminConsoleQueryKeys = { * - Determine whether the current user can access certain object. * - Provide role-based rendering logic for UI components. * - * @param permissions - The array of objects and actions to validate. + * @param permissions - A key/value map of objects and actions to validate. + * The key is an arbitrary string to identify the permission check, + * and the value is an object containing the action and optional scope. * * @example - * const { data } = useValidateUserPermissions([{ - "action": "act:read", - "scope": "org:OpenedX" - }]); - * if (data[0].allowed) { ... } + * const { isLoading, data } = useUserPermissions({ + * "canRead": { + * "action": "act:read", + * "scope": "org:OpenedX" + * } + * }); + * if (data.canRead) { ... } * */ -export const useValidateUserPermissions = ( - permissions: PermissionValidationRequest[], -) => useQuery({ +export const useUserPermissions = ( + permissions: PermissionValidationQuery, +) => useQuery({ queryKey: adminConsoleQueryKeys.permissions(permissions), queryFn: () => validateUserPermissions(permissions), retry: false, diff --git a/src/authz/types.ts b/src/authz/types.ts index 902df408f4..1d641e2af2 100644 --- a/src/authz/types.ts +++ b/src/authz/types.ts @@ -1,8 +1,16 @@ -export interface PermissionValidationRequest { +export interface PermissionValidationRequestItem { action: string; scope?: string; } -export interface PermissionValidationResponse extends PermissionValidationRequest { +export interface PermissionValidationResponseItem extends PermissionValidationRequestItem { allowed: boolean; } + +export interface PermissionValidationQuery { + [permissionKey: string]: PermissionValidationRequestItem; +} + +export interface PermissionValidationAnswer { + [permissionKey: string]: boolean; +} diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index d948921ad3..cc3476c9fb 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -7,7 +7,7 @@ import { useState, } from 'react'; import { useParams } from 'react-router-dom'; -import { useValidateUserPermissions } from '@src/authz/data/apiHooks'; +import { useUserPermissions } from '@src/authz/data/apiHooks'; import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants'; import { ContainerType } from '../../../generic/key-utils'; @@ -16,10 +16,6 @@ import type { ContentLibrary, BlockTypeMetadata } from '../../data/api'; import { useContentLibrary } from '../../data/apiHooks'; import { useComponentPickerContext } from './ComponentPickerContext'; -const LIBRARY_PERMISSIONS = [ - CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, -]; - export interface ComponentEditorInfo { usageKey: string; blockType?:string @@ -114,10 +110,13 @@ export const LibraryProvider = ({ componentPickerMode, } = useComponentPickerContext(); - const permissions = LIBRARY_PERMISSIONS.map(action => ({ action, scope: libraryId })); - - const { isLoading: isLoadingUserPermissions, data: userPermissions } = useValidateUserPermissions(permissions); - const canPublish = userPermissions ? userPermissions[0]?.allowed : false; + const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({ + canPublish: { + action: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, + scope: libraryId, + }, + }); + const canPublish = userPermissions?.canPublish || false; const readOnly = !!componentPickerMode || !libraryData?.canEditLibrary; // Parse the initial collectionId and/or container ID(s) from the current URL params From 546eccf8c51be33e0e2b6616fdb3ab9a3074cb03 Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Mon, 1 Dec 2025 12:18:12 -0600 Subject: [PATCH 12/13] squash!: Fix tests --- src/library-authoring/library-info/LibraryInfo.test.tsx | 8 +------- src/testUtils.tsx | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/library-authoring/library-info/LibraryInfo.test.tsx b/src/library-authoring/library-info/LibraryInfo.test.tsx index abbbeaaa3b..da7870f05a 100644 --- a/src/library-authoring/library-info/LibraryInfo.test.tsx +++ b/src/library-authoring/library-info/LibraryInfo.test.tsx @@ -9,7 +9,6 @@ import { initializeMocks, } from '@src/testUtils'; import { validateUserPermissions } from '@src/authz/data/api'; -import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants'; import { mockContentLibrary } from '../data/api.mocks'; import { getCommitLibraryChangesUrl } from '../data/api'; import { LibraryProvider } from '../common/context/LibraryContext'; @@ -46,12 +45,7 @@ describe('', () => { mockShowToast = mocks.mockShowToast; validateUserPermissionsMock = mocks.validateUserPermissionsMock; - validateUserPermissionsMock.mockResolvedValue([ - { - action: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, - allowed: true, - }, - ]); + validateUserPermissionsMock.mockResolvedValue({ canPublish: true }); }); afterEach(() => { diff --git a/src/testUtils.tsx b/src/testUtils.tsx index cb75904e43..450294994e 100644 --- a/src/testUtils.tsx +++ b/src/testUtils.tsx @@ -196,7 +196,7 @@ export function initializeMocks({ user = defaultUser, initialState = undefined } // Mock user permissions to avoid breaking tests that monitor axios calls // If needed, override the mockResolvedValue in your test - validateUserPermissionsMock = jest.spyOn(authzApi, 'validateUserPermissions').mockResolvedValue([]); + validateUserPermissionsMock = jest.spyOn(authzApi, 'validateUserPermissions').mockResolvedValue({}); return { reduxStore, From 6d8852c97e3bc5164322e2c3d81f498716c859cc Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez Date: Mon, 1 Dec 2025 12:49:22 -0600 Subject: [PATCH 13/13] squash!: Use realistic examples on tests and docs --- src/authz/data/apiHooks.test.tsx | 24 ++++++++++++------------ src/authz/data/apiHooks.ts | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/authz/data/apiHooks.test.tsx b/src/authz/data/apiHooks.test.tsx index e6bf311f1c..03ee5c4b23 100644 --- a/src/authz/data/apiHooks.test.tsx +++ b/src/authz/data/apiHooks.test.tsx @@ -26,17 +26,17 @@ const createWrapper = () => { const singlePermission = { canRead: { - action: 'act:read', - scope: 'lib:test-lib', + action: 'example.read', + scope: 'lib:example-org:test-lib', }, }; const mockValidSinglePermission = [ - { action: 'act:read', scope: 'lib:test-lib', allowed: true }, + { action: 'example.read', scope: 'lib:example-org:test-lib', allowed: true }, ]; const mockInvalidSinglePermission = [ - { action: 'act:read', scope: 'lib:test-lib', allowed: false }, + { action: 'example.read', scope: 'lib:example-org:test-lib', allowed: false }, ]; const mockEmptyPermissions = [ @@ -45,23 +45,23 @@ const mockEmptyPermissions = [ const multiplePermissions = { canRead: { - action: 'act:read', - scope: 'lib:test-lib', + action: 'example.read', + scope: 'lib:example-org:test-lib', }, canWrite: { - action: 'act:write', - scope: 'lib:test-lib', + action: 'example.write', + scope: 'lib:example-org:test-lib', }, }; const mockValidMultiplePermissions = [ - { action: 'act:read', scope: 'lib:test-lib', allowed: true }, - { action: 'act:write', scope: 'lib:test-lib', allowed: true }, + { action: 'example.read', scope: 'lib:example-org:test-lib', allowed: true }, + { action: 'example.write', scope: 'lib:example-org:test-lib', allowed: true }, ]; const mockInvalidMultiplePermissions = [ - { action: 'act:read', scope: 'lib:test-lib', allowed: false }, - { action: 'act:write', scope: 'lib:test-lib', allowed: false }, + { action: 'example.read', scope: 'lib:example-org:test-lib', allowed: false }, + { action: 'example.write', scope: 'lib:example-org:test-lib', allowed: false }, ]; describe('useUserPermissions', () => { diff --git a/src/authz/data/apiHooks.ts b/src/authz/data/apiHooks.ts index 2384c72591..b91d582f57 100644 --- a/src/authz/data/apiHooks.ts +++ b/src/authz/data/apiHooks.ts @@ -19,9 +19,9 @@ const adminConsoleQueryKeys = { * * @example * const { isLoading, data } = useUserPermissions({ - * "canRead": { - * "action": "act:read", - * "scope": "org:OpenedX" + * canRead: { + * action: "content_libraries.view_library", + * scope: "lib:OpenedX:CSPROB" * } * }); * if (data.canRead) { ... }