Skip to content
Draft
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
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');

module.exports = createConfig('eslint');
module.exports = createConfig('eslint', {
rules: {
'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }],
},
});
197 changes: 197 additions & 0 deletions src/authz-module/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import {
getRolesMetadata,
getPermissions,
getResourceTypes,
RESOURCE_TYPES,
RoleOperationErrorStatus,
ROUTES,
libraryRolesMetadata,
courseRolesMetadata,
libraryPermissions,
libraryResourceTypes,
CONTENT_LIBRARY_PERMISSIONS,
COURSE_PERMISSIONS,
} from './constants';

describe('ROUTES', () => {
it('defines the expected paths', () => {
expect(ROUTES.LIBRARIES_TEAM_PATH).toBe('/libraries/:libraryId');
expect(ROUTES.LIBRARIES_USER_PATH).toBe('/libraries/:libraryId/:username');
expect(ROUTES.ASSIGN_ROLE_WIZARD_PATH).toBe('/assign-role');
});
});

describe('RoleOperationErrorStatus', () => {
it('has expected enum values', () => {
expect(RoleOperationErrorStatus.USER_NOT_FOUND).toBe('user_not_found');
expect(RoleOperationErrorStatus.USER_ALREADY_HAS_ROLE).toBe('user_already_has_role');
expect(RoleOperationErrorStatus.USER_DOES_NOT_HAVE_ROLE).toBe('user_does_not_have_role');
expect(RoleOperationErrorStatus.ROLE_ASSIGNMENT_ERROR).toBe('role_assignment_error');
expect(RoleOperationErrorStatus.ROLE_REMOVAL_ERROR).toBe('role_removal_error');
});
});

describe('getRolesMetadata', () => {
it('returns library roles for LIBRARY resource type', () => {
expect(getRolesMetadata(RESOURCE_TYPES.LIBRARY)).toEqual(libraryRolesMetadata);
expect(getRolesMetadata(RESOURCE_TYPES.LIBRARY)).toHaveLength(4);
});

it('returns course roles for COURSE resource type', () => {
expect(getRolesMetadata(RESOURCE_TYPES.COURSE)).toEqual(courseRolesMetadata);
expect(getRolesMetadata(RESOURCE_TYPES.COURSE)).toHaveLength(4);
});

it('returns empty array for unknown resource type', () => {
// @ts-expect-error testing invalid input
expect(getRolesMetadata('unknown')).toEqual([]);
});
});

describe('getPermissions', () => {
it('returns library permissions for LIBRARY resource type', () => {
expect(getPermissions(RESOURCE_TYPES.LIBRARY)).toEqual(libraryPermissions);
expect(getPermissions(RESOURCE_TYPES.LIBRARY).length).toBeGreaterThan(0);
});

it('returns empty array for COURSE resource type', () => {
expect(getPermissions(RESOURCE_TYPES.COURSE)).toEqual([]);
});
});

describe('getResourceTypes', () => {
it('returns library resource types for LIBRARY resource type', () => {
expect(getResourceTypes(RESOURCE_TYPES.LIBRARY)).toEqual(libraryResourceTypes);
expect(getResourceTypes(RESOURCE_TYPES.LIBRARY)).toHaveLength(4);
});

it('returns empty array for COURSE resource type', () => {
expect(getResourceTypes(RESOURCE_TYPES.COURSE)).toEqual([]);
});
});

describe('libraryRolesMetadata', () => {
it('includes all expected library roles', () => {
const roles = libraryRolesMetadata.map((r) => r.role);
expect(roles).toContain('library_admin');
expect(roles).toContain('library_author');
expect(roles).toContain('library_contributor');
expect(roles).toContain('library_user');
});

it('all library roles have contextType "library"', () => {
libraryRolesMetadata.forEach((r) => {
expect(r.contextType).toBe('library');
});
});
});

describe('courseRolesMetadata', () => {
it('includes expected course roles', () => {
const roles = courseRolesMetadata.map((r) => r.role);
expect(roles).toContain('course_admin');
expect(roles).toContain('course_staff');
expect(roles).toContain('course_editor');
expect(roles).toContain('course_auditor');
});

it('all course roles have contextType "course"', () => {
courseRolesMetadata.forEach((r) => {
expect(r.contextType).toBe('course');
});
});

it('course_editor and course_auditor are disabled', () => {
const editor = courseRolesMetadata.find((r) => r.role === 'course_editor');
const auditor = courseRolesMetadata.find((r) => r.role === 'course_auditor');
expect(editor?.disabled).toBe(true);
expect(auditor?.disabled).toBe(true);
});

it('course_admin and course_staff are not disabled', () => {
const admin = courseRolesMetadata.find((r) => r.role === 'course_admin');
const staff = courseRolesMetadata.find((r) => r.role === 'course_staff');
expect(admin?.disabled).toBeUndefined();
expect(staff?.disabled).toBeUndefined();
});
});

describe('CONTENT_LIBRARY_PERMISSIONS', () => {
it('defines all expected permission keys', () => {
expect(CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY).toBe('content_libraries.delete_library');
expect(CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS).toBe('content_libraries.manage_library_tags');
expect(CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY).toBe('content_libraries.view_library');
expect(CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT).toBe('content_libraries.edit_library_content');
expect(CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT).toBe('content_libraries.publish_library_content');
expect(CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT).toBe('content_libraries.reuse_library_content');
expect(CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION).toBe('content_libraries.create_library_collection');
expect(CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION).toBe('content_libraries.edit_library_collection');
expect(CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION).toBe('content_libraries.delete_library_collection');
expect(CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM).toBe('content_libraries.manage_library_team');
expect(CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM).toBe('content_libraries.view_library_team');
});
});

describe('COURSE_PERMISSIONS', () => {
it('defines view permissions', () => {
expect(COURSE_PERMISSIONS.VIEW_COURSE).toBe('courses.view_course');
expect(COURSE_PERMISSIONS.VIEW_COURSE_UPDATES).toBe('courses.view_course_updates');
expect(COURSE_PERMISSIONS.VIEW_PAGES_AND_RESOURCES).toBe('courses.view_pages_and_resources');
expect(COURSE_PERMISSIONS.VIEW_FILES).toBe('courses.view_files');
expect(COURSE_PERMISSIONS.VIEW_GRADING_SETTINGS).toBe('courses.view_grading_settings');
expect(COURSE_PERMISSIONS.VIEW_CHECKLISTS).toBe('courses.view_checklists');
expect(COURSE_PERMISSIONS.VIEW_COURSE_TEAM).toBe('courses.view_course_team');
expect(COURSE_PERMISSIONS.VIEW_SCHEDULE_AND_DETAILS).toBe('courses.view_schedule_and_details');
});

it('defines edit permissions', () => {
expect(COURSE_PERMISSIONS.EDIT_COURSE_CONTENT).toBe('courses.edit_course_content');
expect(COURSE_PERMISSIONS.MANAGE_LIBRARY_UPDATES).toBe('courses.manage_library_updates');
expect(COURSE_PERMISSIONS.MANAGE_COURSE_UPDATES).toBe('courses.manage_course_updates');
expect(COURSE_PERMISSIONS.MANAGE_PAGES_AND_RESOURCES).toBe('courses.manage_pages_and_resources');
expect(COURSE_PERMISSIONS.CREATE_FILES).toBe('courses.create_files');
expect(COURSE_PERMISSIONS.EDIT_FILES).toBe('courses.edit_files');
expect(COURSE_PERMISSIONS.EDIT_GRADING_SETTINGS).toBe('courses.edit_grading_settings');
expect(COURSE_PERMISSIONS.MANAGE_GROUP_CONFIGURATIONS).toBe('courses.manage_group_configurations');
expect(COURSE_PERMISSIONS.EDIT_DETAILS).toBe('courses.edit_details');
expect(COURSE_PERMISSIONS.MANAGE_TAGS).toBe('courses.manage_tags');
});

it('defines publish and lifecycle permissions', () => {
expect(COURSE_PERMISSIONS.PUBLISH_COURSE_CONTENT).toBe('courses.publish_course_content');
expect(COURSE_PERMISSIONS.DELETE_FILES).toBe('courses.delete_files');
expect(COURSE_PERMISSIONS.EDIT_SCHEDULE).toBe('courses.edit_schedule');
expect(COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS).toBe('courses.manage_advanced_settings');
expect(COURSE_PERMISSIONS.MANAGE_CERTIFICATES).toBe('courses.manage_certificates');
expect(COURSE_PERMISSIONS.IMPORT_COURSE).toBe('courses.import_course');
expect(COURSE_PERMISSIONS.EXPORT_COURSE).toBe('courses.export_course');
expect(COURSE_PERMISSIONS.EXPORT_TAGS).toBe('courses.export_tags');
});

it('defines team and taxonomy permissions', () => {
expect(COURSE_PERMISSIONS.MANAGE_COURSE_TEAM).toBe('courses.manage_course_team');
expect(COURSE_PERMISSIONS.MANAGE_TAXONOMIES).toBe('courses.manage_taxonomies');
});

it('defines legacy role permissions', () => {
expect(COURSE_PERMISSIONS.LEGACY_STAFF_ROLE_PERMISSIONS).toBe('courses.legacy_staff_role_permissions');
expect(COURSE_PERMISSIONS.LEGACY_INSTRUCTOR_ROLE_PERMISSIONS).toBe('courses.legacy_instructor_role_permissions');
expect(COURSE_PERMISSIONS.LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS).toBe('courses.legacy_limited_staff_role_permissions');
expect(COURSE_PERMISSIONS.LEGACY_DATA_RESEARCHER_PERMISSIONS).toBe('courses.legacy_data_researcher_permissions');
expect(COURSE_PERMISSIONS.LEGACY_BETA_TESTER_PERMISSIONS).toBe('courses.legacy_beta_tester_permissions');
});
});

describe('getPermissions default case', () => {
it('returns empty array for unknown resource type', () => {
// @ts-expect-error testing invalid input
expect(getPermissions('unknown')).toEqual([]);
});
});

describe('getResourceTypes default case', () => {
it('returns empty array for unknown resource type', () => {
// @ts-expect-error testing invalid input
expect(getResourceTypes('unknown')).toEqual([]);
});
});
161 changes: 161 additions & 0 deletions src/authz-module/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const ROUTES = {
LIBRARIES_TEAM_PATH: '/libraries/:libraryId',
LIBRARIES_USER_PATH: '/libraries/:libraryId/:username',
ASSIGN_ROLE_WIZARD_PATH: '/assign-role',
};

export enum RoleOperationErrorStatus {
Expand All @@ -10,3 +11,163 @@ export enum RoleOperationErrorStatus {
ROLE_ASSIGNMENT_ERROR = 'role_assignment_error',
ROLE_REMOVAL_ERROR = 'role_removal_error',
}

// Content Library Permission Keys
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',
};

// 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 = [
{
role: 'library_admin', name: 'Library Admin', description: 'Can create and manage content libraries, including access and structure.', contextType: 'library',
},
{
role: 'library_author', name: 'Library Author', description: 'Can create and edit library content, but cannot manage access.', contextType: 'library',
},
{
role: 'library_contributor', name: 'Library Contributor', description: 'Can contribute and update library content shared with them.', contextType: 'library',
},
{
role: 'library_user', name: 'Library User', description: 'Can view and use library content, but cannot edit it.', contextType: 'library',
},
];

export const libraryResourceTypes = [
{ 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 = [
{ key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY, resource: 'library', description: 'Allows the user to delete the library and all its contents.' },
{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS, resource: 'library', description: 'Add or remove tags from content.' },
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY, resource: 'library', description: 'View content, search, filter, and sort within the library.' },

{ key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT, resource: 'library_content', description: 'Edit content in draft mode' },
{ key: CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT, resource: 'library_content', description: 'Publish content, making it available for reuse' },
{ key: CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT, resource: 'library_content', description: 'Reuse published content within a course.' },

{ key: CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Create new collections within a library.' },
{ key: CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Add or remove content from existing collections.' },
{ key: CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION, resource: 'library_collection', description: 'Delete entire collections from the library.' },

{ key: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM, resource: 'library_team', description: 'Add, remove, and assign roles to users within the library.' },
{ key: CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM, resource: 'library_team', description: 'View the list of users who have access to the library.' },
];

// Course Permission Keys
export const COURSE_PERMISSIONS = {
// View permissions (Course Auditor)
VIEW_COURSE: 'courses.view_course',
VIEW_COURSE_UPDATES: 'courses.view_course_updates',
VIEW_PAGES_AND_RESOURCES: 'courses.view_pages_and_resources',
VIEW_FILES: 'courses.view_files',
VIEW_GRADING_SETTINGS: 'courses.view_grading_settings',
VIEW_CHECKLISTS: 'courses.view_checklists',
VIEW_COURSE_TEAM: 'courses.view_course_team',
VIEW_SCHEDULE_AND_DETAILS: 'courses.view_schedule_and_details',

// Edit permissions (Course Editor)
EDIT_COURSE_CONTENT: 'courses.edit_course_content',
MANAGE_LIBRARY_UPDATES: 'courses.manage_library_updates',
MANAGE_COURSE_UPDATES: 'courses.manage_course_updates',
MANAGE_PAGES_AND_RESOURCES: 'courses.manage_pages_and_resources',
CREATE_FILES: 'courses.create_files',
EDIT_FILES: 'courses.edit_files',
EDIT_GRADING_SETTINGS: 'courses.edit_grading_settings',
MANAGE_GROUP_CONFIGURATIONS: 'courses.manage_group_configurations',
EDIT_DETAILS: 'courses.edit_details',
MANAGE_TAGS: 'courses.manage_tags',

// Publish & lifecycle permissions (Course Staff)
PUBLISH_COURSE_CONTENT: 'courses.publish_course_content',
DELETE_FILES: 'courses.delete_files',
EDIT_SCHEDULE: 'courses.edit_schedule',
MANAGE_ADVANCED_SETTINGS: 'courses.manage_advanced_settings',
MANAGE_CERTIFICATES: 'courses.manage_certificates',
IMPORT_COURSE: 'courses.import_course',
EXPORT_COURSE: 'courses.export_course',
EXPORT_TAGS: 'courses.export_tags',

// Team & taxonomy permissions (Course Admin only)
MANAGE_COURSE_TEAM: 'courses.manage_course_team',
MANAGE_TAXONOMIES: 'courses.manage_taxonomies',

// Legacy role permissions
LEGACY_STAFF_ROLE_PERMISSIONS: 'courses.legacy_staff_role_permissions',
LEGACY_INSTRUCTOR_ROLE_PERMISSIONS: 'courses.legacy_instructor_role_permissions',
LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS: 'courses.legacy_limited_staff_role_permissions',
LEGACY_DATA_RESEARCHER_PERMISSIONS: 'courses.legacy_data_researcher_permissions',
LEGACY_BETA_TESTER_PERMISSIONS: 'courses.legacy_beta_tester_permissions',
};

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

export type ResourceType = typeof RESOURCE_TYPES[keyof typeof RESOURCE_TYPES];

export const courseRolesMetadata = [
{
role: 'course_admin', name: 'Course Admin', description: 'Can manage the course team and all course settings.', contextType: 'course',
},
{
role: 'course_staff', name: 'Course Staff', description: 'Can publish content and manage the course lifecycle in Studio.', contextType: 'course',
},
{
role: 'course_editor', name: 'Course Editor', description: 'Can create and edit course content, but cannot publish or change critical course settings.', contextType: 'course', disabled: true,
},
{
role: 'course_auditor', name: 'Course Auditor', description: 'Can view course content and settings, but cannot make changes.', contextType: 'course', disabled: true,
},
];

// Get roles metadata by resource type
export const getRolesMetadata = (resourceType: ResourceType) => {
switch (resourceType) {
case RESOURCE_TYPES.LIBRARY:
return libraryRolesMetadata;
case RESOURCE_TYPES.COURSE:
return courseRolesMetadata;
default:
return [];
}
};

// Get permissions by resource type
export const getPermissions = (resourceType: ResourceType) => {
switch (resourceType) {
case RESOURCE_TYPES.LIBRARY:
return libraryPermissions;
default:
return [];
}
};

// Get resource types by resource type
export const getResourceTypes = (resourceType: ResourceType) => {
switch (resourceType) {
case RESOURCE_TYPES.LIBRARY:
return libraryResourceTypes;
default:
return [];
}
};
Loading
Loading