feat: update state to return the roles and permissions metadata
This commit is contained in:
committed by
Adolfo R. Brandes
parent
6d8f6fa1c7
commit
6316586cc0
@@ -8,6 +8,12 @@ export interface GetTeamMembersResponse {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export type PermissionsByRole = {
|
||||
key: string;
|
||||
permissions: string[];
|
||||
userCount: number;
|
||||
};
|
||||
|
||||
// TODO: replece api path once is created
|
||||
export const getTeamMembers = async (object: string): Promise<TeamMember[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getApiUrl(`/api/authz/v1/roles/users?scope=${object}`));
|
||||
@@ -24,3 +30,10 @@ export const getLibrary = async (libraryId: string): Promise<LibraryMetadata> =>
|
||||
slug: data.slug,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPermissionsByRole = async (scope: string): Promise<PermissionsByRole[]> => {
|
||||
const url = new URL(getApiUrl('/api/authz/v1/roles'));
|
||||
url.searchParams.append('scope', scope);
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return camelCaseObject(data);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ReactNode } from 'react';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { useLibrary, useTeamMembers } from './hooks';
|
||||
import { useLibrary, usePermissionsByRole, useTeamMembers } from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
@@ -123,3 +123,36 @@ describe('useLibrary', () => {
|
||||
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePermissionsByRole', () => {
|
||||
it('fetches roles for a given scope', async () => {
|
||||
const mockRoles = [
|
||||
{ key: 'admin', permissions: ['perm1'], userCount: 1 },
|
||||
{ key: 'user', permissions: ['perm2'], userCount: 2 },
|
||||
];
|
||||
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
get: jest.fn().mockResolvedValue({ data: mockRoles }),
|
||||
});
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => usePermissionsByRole('lib'), { wrapper });
|
||||
await waitFor(() => result.current.data !== undefined);
|
||||
expect(result.current.data).toEqual(mockRoles);
|
||||
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns error if getRoles fails', async () => {
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
get: jest.fn().mockRejectedValue(new Error('Not found')),
|
||||
});
|
||||
const wrapper = createWrapper();
|
||||
try {
|
||||
act(() => {
|
||||
renderHook(() => usePermissionsByRole('lib'), { wrapper });
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e).toEqual(new Error('Not found'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { appId } from '@src/constants';
|
||||
import { LibraryMetadata, TeamMember } from '@src/types';
|
||||
import { getLibrary, getTeamMembers } from './api';
|
||||
import {
|
||||
getLibrary, getPermissionsByRole, getTeamMembers, PermissionsByRole,
|
||||
} from './api';
|
||||
|
||||
const authzQueryKeys = {
|
||||
all: [appId, 'authz'] as const,
|
||||
teamMembers: (object: string) => [...authzQueryKeys.all, 'teamMembers', object] as const,
|
||||
permissionsByRole: (scope: string) => [...authzQueryKeys.all, 'permissionsByRole', scope] as const,
|
||||
library: (libraryId: string) => [...authzQueryKeys.all, 'library', libraryId] as const,
|
||||
};
|
||||
|
||||
@@ -26,6 +29,23 @@ export const useTeamMembers = (object: string) => useQuery<TeamMember[], Error>(
|
||||
staleTime: 1000 * 60 * 30, // refetch after 30 minutes
|
||||
});
|
||||
|
||||
/**
|
||||
* React Query hook to fetch all the roles for the specific object/scope.
|
||||
* It retrieves the full list of roles with the corresponding permissions.
|
||||
*
|
||||
* @param scope - The unique identifier of the object/scope
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data: roles, isLoading, isError } = useTeamMembers('lib:123');
|
||||
* ```
|
||||
*/
|
||||
export const usePermissionsByRole = (scope: string) => useSuspenseQuery<PermissionsByRole[], Error>({
|
||||
queryKey: authzQueryKeys.permissionsByRole(scope),
|
||||
queryFn: () => getPermissionsByRole(scope),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* React Query hook to retrieve the information of the current library.
|
||||
*
|
||||
|
||||
41
src/authz-module/libraries-manager/constants.ts
Normal file
41
src/authz-module/libraries-manager/constants.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { PermissionMetadata, ResourceMetadata, RoleMetadata } from 'types';
|
||||
|
||||
// Note: this information will eventually come from the backend API
|
||||
// but for the MVP we decided to manage it in the frontend
|
||||
export const libraryRolesMetadata: RoleMetadata[] = [
|
||||
{ key: 'library_admin', name: 'Library Admin', description: 'The Library Admin has full control over the library, including managing users, modifying content, and handling publishing workflows. They ensure content is properly maintained and accessible as needed.' },
|
||||
{ key: 'library_author', name: 'Library Author', description: 'The Library Author is responsible for creating, editing, and publishing content within a library. They can manage tags and collections but cannot delete libraries or manage users.' },
|
||||
{ key: 'library_collaborator', name: 'Library Collaborator', description: 'The Library Collaborator can create and edit content within a library but cannot publish it. They support the authoring process while leaving final publishing to Authors or Admins.' },
|
||||
{ key: 'library_user', name: 'Library User', description: 'The Library User can view and reuse content but cannot edit or delete any resource.' },
|
||||
];
|
||||
|
||||
export const libraryResourceTypes: ResourceMetadata[] = [
|
||||
{ key: 'library', label: 'Library', description: 'Permissions related to the library as a whole.' },
|
||||
{ key: 'library_content', label: 'Content', description: 'Permissions to create, edit, delete, and publish individual content items within the library.' },
|
||||
{ key: 'library_collection', label: 'Collection', description: 'Permissions to create, edit, and delete content collections within the library.' },
|
||||
{ key: 'library_team', label: 'Team', description: 'Permissions to manage user access and roles within the library.' },
|
||||
];
|
||||
|
||||
export const libraryPermissions: PermissionMetadata[] = [
|
||||
{ key: 'create_library', resource: 'library', description: 'Allows the user to create new libraries.' },
|
||||
{ key: 'edit_library', resource: 'library', description: 'Allows the user to modify library settings and metadata.' },
|
||||
{ key: 'delete_library', resource: 'library', description: 'Allows the user to delete the library and all its contents.' },
|
||||
{ key: 'publish_library', resource: 'library', description: 'Publish the library (change from draft mode to published).' },
|
||||
{ key: 'view_library', resource: 'library', description: 'View content, search, filter, and sort within the library.' },
|
||||
{
|
||||
key: 'manage_library_tags', resource: 'library', description: 'Add or remove tags from content.',
|
||||
},
|
||||
|
||||
{ key: 'create_library_content', resource: 'library_content', description: 'Create new components or content units.' },
|
||||
{ key: 'edit_library_content', resource: 'library_content', description: 'Edit content in draft mode' },
|
||||
{ key: 'delete_library_content', resource: 'library_content', description: 'Delete individual content (not collections).' },
|
||||
{ key: 'publish_library_content', resource: 'library_content', description: 'Publish content, making it available for reuse' },
|
||||
{ key: 'reuse_library_content', resource: 'library_content', description: 'Reuse published content within a course.' },
|
||||
|
||||
{ key: 'create_library_collection', resource: 'library_collection', description: 'Create new collections within a library.' },
|
||||
{ key: 'edit_library_collection', resource: 'library_collection', description: 'Add or remove content from existing collections.' },
|
||||
{ key: 'delete_library_collection', resource: 'library_collection', description: 'Delete entire collections from the library.' },
|
||||
|
||||
{ key: 'manage_library_team', resource: 'library_team', description: 'View the list of users who have access to the library.' },
|
||||
{ key: 'view_library_team', resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
|
||||
];
|
||||
@@ -12,6 +12,20 @@ jest.mock('react-router-dom', () => ({
|
||||
jest.mock('@src/data/hooks', () => ({
|
||||
useValidateUserPermissions: jest.fn(),
|
||||
}));
|
||||
jest.mock('@src/authz-module/data/hooks', () => ({
|
||||
usePermissionsByRole: jest.fn().mockReturnValue({
|
||||
data: [
|
||||
{
|
||||
key: 'library_author',
|
||||
permissions: [
|
||||
'view_library_team',
|
||||
'edit_library',
|
||||
],
|
||||
user_count: 12,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
const TestComponent = () => {
|
||||
const context = useLibraryAuthZ();
|
||||
@@ -20,6 +34,9 @@ const TestComponent = () => {
|
||||
<div data-testid="username">{context.username}</div>
|
||||
<div data-testid="libraryId">{context.libraryId}</div>
|
||||
<div data-testid="canManageTeam">{context.canManageTeam ? 'true' : 'false'}</div>
|
||||
<div data-testid="roles">{Array.isArray(context.roles) ? context.roles.length : 'undefined'}</div>
|
||||
<div data-testid="permissions">{Array.isArray(context.permissions) ? context.permissions.length : 'undefined'}</div>
|
||||
<div data-testid="resources">{Array.isArray(context.resources) ? context.resources.length : 'undefined'}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -47,6 +64,9 @@ describe('LibraryAuthZProvider', () => {
|
||||
expect(screen.getByTestId('username')).toHaveTextContent('testuser');
|
||||
expect(screen.getByTestId('libraryId')).toHaveTextContent('lib123');
|
||||
expect(screen.getByTestId('canManageTeam')).toHaveTextContent('true');
|
||||
expect(Number(screen.getByTestId('roles').textContent)).not.toBeNaN();
|
||||
expect(Number(screen.getByTestId('permissions').textContent)).not.toBeNaN();
|
||||
expect(Number(screen.getByTestId('resources').textContent)).not.toBeNaN();
|
||||
});
|
||||
|
||||
it('throws error when user lacks both view and manage permissions', () => {
|
||||
|
||||
@@ -4,8 +4,12 @@ import {
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { useValidateUserPermissions } from '@src/data/hooks';
|
||||
import { usePermissionsByRole } from '@src/authz-module/data/hooks';
|
||||
import { PermissionMetadata, ResourceMetadata, Role } from 'types';
|
||||
import { libraryPermissions, libraryResourceTypes, libraryRolesMetadata } from './constants';
|
||||
|
||||
const LIBRARY_TEAM_PERMISSIONS = ['act:view_library_team', 'act:manage_library_team'];
|
||||
const LIBRARY_AUTHZ_SCOPE = 'lib:*';
|
||||
|
||||
export type AppContextType = {
|
||||
authenticatedUser: {
|
||||
@@ -18,8 +22,9 @@ type LibraryAuthZContextType = {
|
||||
canManageTeam: boolean;
|
||||
username: string;
|
||||
libraryId: string;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
resources: ResourceMetadata[];
|
||||
roles: Role[];
|
||||
permissions: PermissionMetadata[];
|
||||
};
|
||||
|
||||
const LibraryAuthZContext = createContext<LibraryAuthZContextType | undefined>(undefined);
|
||||
@@ -45,13 +50,17 @@ export const LibraryAuthZProvider: React.FC<AuthZProviderProps> = ({ children }:
|
||||
throw new Error('NoAccess');
|
||||
}
|
||||
|
||||
const { data: libraryRoles } = usePermissionsByRole(LIBRARY_AUTHZ_SCOPE);
|
||||
const roles = libraryRoles.map(role => ({ ...role, ...libraryRolesMetadata.find(r => r.key === role.key) } as Role));
|
||||
|
||||
const value = useMemo((): LibraryAuthZContextType => ({
|
||||
username: authenticatedUser.username,
|
||||
libraryId,
|
||||
roles: [],
|
||||
permissions: [],
|
||||
roles,
|
||||
permissions: libraryPermissions,
|
||||
resources: libraryResourceTypes,
|
||||
canManageTeam,
|
||||
}), [libraryId, authenticatedUser.username, canManageTeam]);
|
||||
}), [libraryId, authenticatedUser.username, canManageTeam, roles]);
|
||||
|
||||
return (
|
||||
<LibraryAuthZContext.Provider value={value}>
|
||||
|
||||
23
src/types.ts
23
src/types.ts
@@ -22,6 +22,29 @@ export interface LibraryMetadata {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface RoleMetadata {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
export interface Role extends RoleMetadata {
|
||||
userCount: number;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export type ResourceMetadata = {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type PermissionMetadata = {
|
||||
key: string;
|
||||
resource: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// Paragon table type
|
||||
export interface TableCellValue<T> {
|
||||
row: {
|
||||
|
||||
Reference in New Issue
Block a user