Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/authz-module/authz-home/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { useAllRoleAssignments, useOrgs, useScopes } from '@src/authz-module/data/hooks';
import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext';
import { renderWithAllProviders } from '@src/setupTest';
import userEvent from '@testing-library/user-event';
import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext';
import AuthzHome from './index';
import messages from './messages';

Expand Down
6 changes: 3 additions & 3 deletions src/authz-module/components/AddRoleButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('AddRoleButton', () => {
await user.click(button);

expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?username=${presetUsername}`);
expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?users=${presetUsername}`);
});

it('handles special characters in presetUsername correctly', async () => {
Expand All @@ -88,7 +88,7 @@ describe('AddRoleButton', () => {
await user.click(button);

expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?username=${presetUsername}`);
expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?${new URLSearchParams({ users: presetUsername }).toString()}`);
});
});

Expand Down Expand Up @@ -128,7 +128,7 @@ describe('AddRoleButton', () => {
await user.click(button);

expect(mockNavigate).toHaveBeenCalledTimes(3);
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role?username=testuser');
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role?users=testuser');
});
});
});
10 changes: 6 additions & 4 deletions src/authz-module/components/AddRoleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Plus } from '@openedx/paragon/icons';
import { useNavigate } from 'react-router-dom';

import baseMessages from '@src/authz-module/messages';
import { useNavigate } from 'react-router-dom';
import { buildWizardPath } from '@src/authz-module/constants';

interface AddRoleButtonProps {
presetUsername?: string;
from?: string;
}

const AddRoleButton = ({ presetUsername }: AddRoleButtonProps) => {
const AddRoleButton = ({ presetUsername, from }: AddRoleButtonProps) => {
const intl = useIntl();
const navigate = useNavigate();

const handleClick = () => {
const assignRolePath = `/authz/assign-role${presetUsername ? `?username=${presetUsername}` : ''}`;
navigate(assignRolePath);
const path = buildWizardPath({ from, users: presetUsername });
navigate(path);
};

return (
Expand Down
3 changes: 3 additions & 0 deletions src/authz-module/components/PermissionTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ const mockRoles: Role[] = [
userCount: 0,
permissions: [],
role: '',
contextType: '',
},
{
name: 'Editor',
description: 'Editor role',
userCount: 0,
permissions: [],
role: '',
contextType: '',
},
{
name: 'Viewer',
description: 'Viewer role',
userCount: 0,
permissions: [],
role: '',
contextType: '',
},
];

Expand Down
34 changes: 34 additions & 0 deletions src/authz-module/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { buildWizardPath, ROUTES } from './constants';

const BASE = `${ROUTES.HOME_PATH}${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`;

describe('buildWizardPath', () => {
it('returns the base path when called with no arguments', () => {
expect(buildWizardPath()).toBe(BASE);
});

it('returns the base path when called with an empty options object', () => {
expect(buildWizardPath({})).toBe(BASE);
});

it('appends ?users= when only users is provided', () => {
expect(buildWizardPath({ users: 'alice' })).toBe(`${BASE}?users=alice`);
});

it('appends ?from= when only from is provided', () => {
expect(buildWizardPath({ from: '/authz/libraries/lib:123/alice' }))
.toBe(`${BASE}?from=%2Fauthz%2Flibraries%2Flib%3A123%2Falice`);
});

it('appends both users and from when both are provided', () => {
const result = buildWizardPath({ users: 'alice', from: '/authz/libraries/lib:123/alice' });
const parsed = new URL(result, 'http://x');
expect(parsed.pathname).toBe(BASE);
expect(parsed.searchParams.get('users')).toBe('alice');
expect(parsed.searchParams.get('from')).toBe('/authz/libraries/lib:123/alice');
});

it('omits the query string when users and from are both empty strings', () => {
expect(buildWizardPath({ users: '', from: '' })).toBe(BASE);
});
});
31 changes: 21 additions & 10 deletions src/authz-module/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PermissionMetadata, ResourceMetadata, RoleMetadata } from 'types';
import { PermissionMetadata, ResourceMetadata } from 'types';

export const CONTENT_LIBRARY_PERMISSIONS = {
DELETE_LIBRARY: 'content_libraries.delete_library',
Expand Down Expand Up @@ -62,15 +62,6 @@ export const CONTENT_COURSE_PERMISSIONS = {
VIEW_COURSE_GLOBAL_STAFF_SUPER_ADMINS: 'courses.view_global_staff_and_superadmins',
};

// 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[] = [
{ role: '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.' },
{ role: '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.' },
{ role: 'library_contributor', name: 'Library Contributor', description: 'The Library Contributor 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.' },
{ role: '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.' },
Expand Down Expand Up @@ -105,9 +96,21 @@ export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
}));

export const ROUTES = {
HOME_PATH: '/authz',
LIBRARIES_TEAM_PATH: '/libraries/:libraryId',
LIBRARIES_USER_PATH: '/libraries/:libraryId/:username',
AUDIT_USER_PATH: '/user/:username',
ASSIGN_ROLE_WIZARD_PATH: '/assign-role',
};

export const buildWizardPath = (options?: { users?: string; from?: string }) => {
const base = `${ROUTES.HOME_PATH}${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`;
if (!options) { return base; }
const params = new URLSearchParams();
if (options.users) { params.set('users', options.users); }
if (options.from) { params.set('from', options.from); }
const query = params.toString();
return query ? `${base}?${query}` : base;
};

export enum RoleOperationErrorStatus {
Expand Down Expand Up @@ -141,3 +144,11 @@ export const TABLE_DEFAULT_PAGE_SIZE = 10;

export const DEFAULT_FILTER_PAGE_SIZE = 5;
export const ADMIN_ROLES = ['course_admin', 'library_admin'];

// Resource Type Definitions
export const RESOURCE_TYPES = {
LIBRARY: 'library',
COURSE: 'course',
} as const;

export type ResourceType = typeof RESOURCE_TYPES[keyof typeof RESOURCE_TYPES];
23 changes: 23 additions & 0 deletions src/authz-module/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ export interface GetScopesResponse {
previous: string | null;
results:Array<Scope>;
}
export type ValidateUsersRequest = {
users: string[];
};

export type ValidateUsersResponse = {
validUsers: string[];
invalidUsers: string[];
summary: {
total: number;
validCount: number;
invalidCount: number;
};
};

export const getTeamMembers = async (object: string, querySettings: QuerySettings): Promise<GetTeamMembersResponse> => {
const url = new URL(getApiUrl(`/api/authz/v1/roles/users/?scope=${object}`));
Expand Down Expand Up @@ -111,6 +124,16 @@ export const assignTeamMembersRole = async (
return camelCaseObject(res.data);
};

export const validateUsers = async (
data: ValidateUsersRequest,
): Promise<ValidateUsersResponse> => {
const res = await getAuthenticatedHttpClient().post(
getApiUrl('/api/authz/v1/users/validate/'),
data,
);
return camelCaseObject(res.data);
};

// TODO: this should be replaced in the future with Console API
export const getLibrary = async (libraryId: string): Promise<LibraryMetadata> => {
const { data } = await getAuthenticatedHttpClient().get(getStudioApiUrl(`/api/libraries/v2/${libraryId}/`));
Expand Down
Loading