Skip to content

Commit 59905aa

Browse files
committed
feat: Implement querying openedx-authz for publish permissions
1 parent 2215fc5 commit 59905aa

File tree

7 files changed

+93
-4
lines changed

7 files changed

+93
-4
lines changed

src/authz/constants.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const appId = 'org.openedx.frontend.app.adminConsole';
2+
3+
export const CONTENT_LIBRARY_PERMISSIONS = {
4+
DELETE_LIBRARY: 'content_libraries.delete_library',
5+
MANAGE_LIBRARY_TAGS: 'content_libraries.manage_library_tags',
6+
VIEW_LIBRARY: 'content_libraries.view_library',
7+
8+
EDIT_LIBRARY_CONTENT: 'content_libraries.edit_library_content',
9+
PUBLISH_LIBRARY_CONTENT: 'content_libraries.publish_library_content',
10+
REUSE_LIBRARY_CONTENT: 'content_libraries.reuse_library_content',
11+
12+
CREATE_LIBRARY_COLLECTION: 'content_libraries.create_library_collection',
13+
EDIT_LIBRARY_COLLECTION: 'content_libraries.edit_library_collection',
14+
DELETE_LIBRARY_COLLECTION: 'content_libraries.delete_library_collection',
15+
16+
MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team',
17+
VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team',
18+
};

src/authz/data/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
2+
import { PermissionValidationRequest, PermissionValidationResponse } from '@src/authz/types';
3+
import { getApiUrl } from './utils';
4+
5+
export const validateUserPermissions = async (
6+
validations: PermissionValidationRequest[],
7+
): Promise<PermissionValidationResponse[]> => {
8+
const { data } = await getAuthenticatedHttpClient().post(getApiUrl('/api/authz/v1/permissions/validate/me'), validations);
9+
return data;
10+
};

src/authz/data/hooks.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { PermissionValidationRequest, PermissionValidationResponse } from '@src/authz/types';
3+
import { appId } from '@src/authz/constants';
4+
import { validateUserPermissions } from './api';
5+
6+
const adminConsoleQueryKeys = {
7+
all: [appId] as const,
8+
permissions: (permissions: PermissionValidationRequest[]) => [...adminConsoleQueryKeys.all, 'validatePermissions', permissions] as const,
9+
};
10+
11+
/**
12+
* React Query hook to validate if the current user has permissions over a certain object in the instance.
13+
* It helps to:
14+
* - Determine whether the current user can access certain object.
15+
* - Provide role-based rendering logic for UI components.
16+
*
17+
* @param permissions - The array of objects and actions to validate.
18+
*
19+
* @example
20+
* const { data } = useValidateUserPermissions([{
21+
"action": "act:read",
22+
"object": "lib:test-lib",
23+
"scope": "org:OpenedX"
24+
}]);
25+
* if (data[0].allowed) { ... }
26+
*
27+
*/
28+
export const useValidateUserPermissions = (
29+
permissions: PermissionValidationRequest[],
30+
) => useQuery<PermissionValidationResponse[], Error>({
31+
queryKey: adminConsoleQueryKeys.permissions(permissions),
32+
queryFn: () => validateUserPermissions(permissions),
33+
retry: false,
34+
});

src/authz/data/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { getConfig } from '@edx/frontend-platform';
2+
3+
export const getApiUrl = (path: string) => `${getConfig().LMS_BASE_URL}${path || ''}`;
4+
export const getStudioApiUrl = (path: string) => `${getConfig().STUDIO_BASE_URL}${path || ''}`;

src/authz/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface PermissionValidationRequest {
2+
action: string;
3+
scope?: string;
4+
}
5+
6+
export interface PermissionValidationResponse extends PermissionValidationRequest {
7+
allowed: boolean;
8+
}

src/library-authoring/common/context/LibraryContext.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import type { ComponentPicker } from '../../component-picker';
1313
import type { ContentLibrary, BlockTypeMetadata } from '../../data/api';
1414
import { useContentLibrary } from '../../data/apiHooks';
1515
import { useComponentPickerContext } from './ComponentPickerContext';
16+
import { useValidateUserPermissions } from '@src/authz/data/hooks';
17+
import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants';
18+
19+
const LIBRARY_PERMISSIONS = [
20+
CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT,
21+
];
1622

1723
export interface ComponentEditorInfo {
1824
usageKey: string;
@@ -25,6 +31,7 @@ export type LibraryContextData = {
2531
libraryId: string;
2632
libraryData?: ContentLibrary;
2733
readOnly: boolean;
34+
canPublish: boolean;
2835
isLoadingLibraryData: boolean;
2936
/** The ID of the current collection/container, on the sidebar OR page */
3037
collectionId: string | undefined;
@@ -107,6 +114,11 @@ export const LibraryProvider = ({
107114
componentPickerMode,
108115
} = useComponentPickerContext();
109116

117+
const permissions = LIBRARY_PERMISSIONS.map(action => ({ action, scope: libraryId }));
118+
119+
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useValidateUserPermissions(permissions);
120+
const canPublish = userPermissions ? userPermissions[0]?.allowed : false;
121+
// TODO change to use canEdit from userPermissions later
110122
const readOnly = !!componentPickerMode || !libraryData?.canEditLibrary;
111123

112124
// Parse the initial collectionId and/or container ID(s) from the current URL params
@@ -131,7 +143,8 @@ export const LibraryProvider = ({
131143
containerId,
132144
setContainerId,
133145
readOnly,
134-
isLoadingLibraryData,
146+
canPublish,
147+
isLoadingLibraryData: isLoadingLibraryData || isLoadingUserPermissions,
135148
showOnlyPublished,
136149
extraFilter,
137150
isCreateCollectionModalOpen,
@@ -154,7 +167,9 @@ export const LibraryProvider = ({
154167
containerId,
155168
setContainerId,
156169
readOnly,
170+
canPublish,
157171
isLoadingLibraryData,
172+
isLoadingUserPermissions,
158173
showOnlyPublished,
159174
extraFilter,
160175
isCreateCollectionModalOpen,

src/library-authoring/library-info/LibraryPublishStatus.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import messages from './messages';
1212

1313
const LibraryPublishStatus = () => {
1414
const intl = useIntl();
15-
const { libraryData, readOnly } = useLibraryContext();
15+
const { libraryData, readOnly, canPublish } = useLibraryContext();
1616
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
1717

1818
const commitLibraryChanges = useCommitLibraryChanges();
@@ -51,10 +51,10 @@ const LibraryPublishStatus = () => {
5151
<>
5252
<StatusWidget
5353
{...libraryData}
54-
onCommit={!readOnly ? commit : undefined}
54+
onCommit={!readOnly && canPublish ? commit : undefined}
5555
onCommitStatus={commitLibraryChanges.status}
5656
onCommitLabel={intl.formatMessage(messages.publishLibraryButtonLabel)}
57-
onRevert={!readOnly ? openConfirmModal : undefined}
57+
onRevert={!readOnly && canPublish ? openConfirmModal : undefined}
5858
/>
5959
<DeleteModal
6060
isOpen={isConfirmModalOpen}

0 commit comments

Comments
 (0)