diff --git a/.gitignore b/.gitignore index 99ce4cbb..2735fac7 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ requirements/private.txt # Persistent database files *.sqlite3 *.db + +# IDE +.vscode/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 051da5dd..2488c34b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,18 @@ Change Log Unreleased ********** +1.23.0 - 2026-08-13 +******************* + +Added +===== + +* Added ``courses.view_advanced_settings`` permission for read-only access to advanced settings. +* Added ``courses.view_certificates`` permission for read-only access to certificates. +* Added ``courses.view_group_configurations`` permission for read-only access to group configurations. +* Added ``courses.view_library_updates`` permission for read-only access to library updates. +* All four view permissions are granted to all four course roles (Admin, Staff, Editor, Auditor). + 1.22.0 - 2026-08-12 ******************* diff --git a/docs/concepts/core_roles_and_permissions/course_roles.rst b/docs/concepts/core_roles_and_permissions/course_roles.rst index afebebd9..2a9b4d13 100644 --- a/docs/concepts/core_roles_and_permissions/course_roles.rst +++ b/docs/concepts/core_roles_and_permissions/course_roles.rst @@ -42,6 +42,7 @@ Course Access & Content Library Updates ======================== +- **View library updates** (``courses.view_library_updates``): Allows users to view library updates in read-only mode. - **Manage library updates** (``courses.manage_library_updates``): Allows users to accept or reject library updates in Studio. Course Updates & Handouts @@ -76,6 +77,7 @@ Course Team & Groups ==================== - **View course team** (``courses.view_course_team``): Allows users to view the course team roster. - **Manage course team** (``courses.manage_course_team``): Allows users to edit course team membership and roles. +- **View group configurations** (``courses.view_group_configurations``): Allows users to view content groups. - **Manage group configuration** (``courses.manage_group_configurations``): Allows users to manage content groups. Tags & Taxonomies @@ -85,8 +87,10 @@ Tags & Taxonomies Advanced & Certificates ======================= -- **Manage advanced settings** (``courses.manage_advanced_settings``): Allows users to access and edit advanced settings. -- **Manage certificates** (``courses.manage_certificates``): Allows users to access and edit certificates. +- **View advanced settings** (``courses.view_advanced_settings``): Allows users to view advanced settings. +- **Manage advanced settings** (``courses.manage_advanced_settings``): Allows users to edit advanced settings. +- **View certificates** (``courses.view_certificates``): Allows users to view certificates. +- **Manage certificates** (``courses.manage_certificates``): Allows users to edit certificates. Import / Export ================ @@ -121,7 +125,9 @@ Roles and Permissions Summary Table courses.manage_course_updates ✅ ✅ ✅ ❌ **Advanced & Certificates** --------------------------------------------- -------------- -------------- --------------------- -------------- + courses.view_advanced_settings ✅ ✅ ✅ ✅ courses.manage_advanced_settings ✅ ✅ ❌ ❌ + courses.view_certificates ✅ ✅ ✅ ✅ courses.manage_certificates ✅ ✅ ❌ ❌ **Access & Content** --------------------------------------------- -------------- -------------- --------------------- -------------- @@ -142,6 +148,7 @@ Roles and Permissions Summary Table courses.edit_details ✅ ✅ ✅ ❌ **Library Updates** --------------------------------------------- -------------- -------------- --------------------- -------------- + courses.view_library_updates ✅ ✅ ✅ ✅ courses.manage_library_updates ✅ ✅ ✅ ❌ **Grading** --------------------------------------------- -------------- -------------- --------------------- -------------- @@ -159,6 +166,7 @@ Roles and Permissions Summary Table **Team & Groups** --------------------------------------------- -------------- -------------- --------------------- -------------- courses.view_course_team ✅ ✅ ✅ ✅ + courses.view_group_configurations ✅ ✅ ✅ ✅ courses.manage_group_configurations ✅ ✅ ✅ ❌ courses.manage_course_team ✅ ❌ ❌ ❌ **Other** diff --git a/openedx_authz/__init__.py b/openedx_authz/__init__.py index 553f187a..52cd779d 100644 --- a/openedx_authz/__init__.py +++ b/openedx_authz/__init__.py @@ -4,6 +4,6 @@ import os -__version__ = "1.22.0" +__version__ = "1.23.0" ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) diff --git a/openedx_authz/constants/permissions.py b/openedx_authz/constants/permissions.py index ed1ba42c..4921d1c8 100644 --- a/openedx_authz/constants/permissions.py +++ b/openedx_authz/constants/permissions.py @@ -78,6 +78,11 @@ effect="allow", ) +COURSES_VIEW_LIBRARY_UPDATES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_library_updates"), + effect="allow", +) + COURSES_MANAGE_LIBRARY_UPDATES = PermissionData( action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_library_updates"), effect="allow", @@ -158,16 +163,31 @@ effect="allow", ) +COURSES_VIEW_GROUP_CONFIGURATIONS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_group_configurations"), + effect="allow", +) + COURSES_MANAGE_GROUP_CONFIGURATIONS = PermissionData( action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_group_configurations"), effect="allow", ) +COURSES_VIEW_ADVANCED_SETTINGS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_advanced_settings"), + effect="allow", +) + COURSES_MANAGE_ADVANCED_SETTINGS = PermissionData( action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_advanced_settings"), effect="allow", ) +COURSES_VIEW_CERTIFICATES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_certificates"), + effect="allow", +) + COURSES_MANAGE_CERTIFICATES = PermissionData( action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_certificates"), effect="allow", diff --git a/openedx_authz/constants/roles.py b/openedx_authz/constants/roles.py index 99badb03..1d083f01 100644 --- a/openedx_authz/constants/roles.py +++ b/openedx_authz/constants/roles.py @@ -69,6 +69,10 @@ permissions.COURSES_VIEW_CHECKLISTS, permissions.COURSES_VIEW_COURSE_TEAM, permissions.COURSES_VIEW_SCHEDULE_AND_DETAILS, + permissions.COURSES_VIEW_ADVANCED_SETTINGS, + permissions.COURSES_VIEW_CERTIFICATES, + permissions.COURSES_VIEW_GROUP_CONFIGURATIONS, + permissions.COURSES_VIEW_LIBRARY_UPDATES, ] COURSE_AUDITOR = RoleData(external_key="course_auditor", permissions=COURSE_AUDITOR_PERMISSIONS) @@ -82,6 +86,10 @@ permissions.COURSES_VIEW_CHECKLISTS, permissions.COURSES_VIEW_COURSE_TEAM, permissions.COURSES_VIEW_SCHEDULE_AND_DETAILS, + permissions.COURSES_VIEW_ADVANCED_SETTINGS, + permissions.COURSES_VIEW_CERTIFICATES, + permissions.COURSES_VIEW_GROUP_CONFIGURATIONS, + permissions.COURSES_VIEW_LIBRARY_UPDATES, permissions.COURSES_EDIT_COURSE_CONTENT, permissions.COURSES_MANAGE_LIBRARY_UPDATES, permissions.COURSES_MANAGE_COURSE_UPDATES, @@ -106,6 +114,10 @@ permissions.COURSES_VIEW_CHECKLISTS, permissions.COURSES_VIEW_COURSE_TEAM, permissions.COURSES_VIEW_SCHEDULE_AND_DETAILS, + permissions.COURSES_VIEW_ADVANCED_SETTINGS, + permissions.COURSES_VIEW_CERTIFICATES, + permissions.COURSES_VIEW_GROUP_CONFIGURATIONS, + permissions.COURSES_VIEW_LIBRARY_UPDATES, permissions.COURSES_EDIT_COURSE_CONTENT, permissions.COURSES_MANAGE_LIBRARY_UPDATES, permissions.COURSES_MANAGE_COURSE_UPDATES, @@ -140,6 +152,10 @@ permissions.COURSES_VIEW_CHECKLISTS, permissions.COURSES_VIEW_COURSE_TEAM, permissions.COURSES_VIEW_SCHEDULE_AND_DETAILS, + permissions.COURSES_VIEW_ADVANCED_SETTINGS, + permissions.COURSES_VIEW_CERTIFICATES, + permissions.COURSES_VIEW_GROUP_CONFIGURATIONS, + permissions.COURSES_VIEW_LIBRARY_UPDATES, permissions.COURSES_EDIT_COURSE_CONTENT, permissions.COURSES_MANAGE_LIBRARY_UPDATES, permissions.COURSES_MANAGE_COURSE_UPDATES, diff --git a/openedx_authz/engine/config/authz.policy b/openedx_authz/engine/config/authz.policy index 4dfb3d1b..9172df30 100644 --- a/openedx_authz/engine/config/authz.policy +++ b/openedx_authz/engine/config/authz.policy @@ -81,6 +81,10 @@ p, role^course_auditor, act^courses.view_grading_settings, course-v1^*, allow p, role^course_auditor, act^courses.view_checklists, course-v1^*, allow p, role^course_auditor, act^courses.view_course_team, course-v1^*, allow p, role^course_auditor, act^courses.view_schedule_and_details, course-v1^*, allow +p, role^course_auditor, act^courses.view_advanced_settings, course-v1^*, allow +p, role^course_auditor, act^courses.view_certificates, course-v1^*, allow +p, role^course_auditor, act^courses.view_group_configurations, course-v1^*, allow +p, role^course_auditor, act^courses.view_library_updates, course-v1^*, allow # Course Editor Role Policies p, role^course_editor, act^courses.view_course, course-v1^*, allow @@ -91,6 +95,10 @@ p, role^course_editor, act^courses.view_grading_settings, course-v1^*, allow p, role^course_editor, act^courses.view_checklists, course-v1^*, allow p, role^course_editor, act^courses.view_course_team, course-v1^*, allow p, role^course_editor, act^courses.view_schedule_and_details, course-v1^*, allow +p, role^course_editor, act^courses.view_advanced_settings, course-v1^*, allow +p, role^course_editor, act^courses.view_certificates, course-v1^*, allow +p, role^course_editor, act^courses.view_group_configurations, course-v1^*, allow +p, role^course_editor, act^courses.view_library_updates, course-v1^*, allow p, role^course_editor, act^courses.edit_course_content, course-v1^*, allow p, role^course_editor, act^courses.manage_library_updates, course-v1^*, allow p, role^course_editor, act^courses.manage_course_updates, course-v1^*, allow @@ -112,6 +120,10 @@ p, role^course_staff, act^courses.view_grading_settings, course-v1^*, allow p, role^course_staff, act^courses.view_checklists, course-v1^*, allow p, role^course_staff, act^courses.view_course_team, course-v1^*, allow p, role^course_staff, act^courses.view_schedule_and_details, course-v1^*, allow +p, role^course_staff, act^courses.view_advanced_settings, course-v1^*, allow +p, role^course_staff, act^courses.view_certificates, course-v1^*, allow +p, role^course_staff, act^courses.view_group_configurations, course-v1^*, allow +p, role^course_staff, act^courses.view_library_updates, course-v1^*, allow p, role^course_staff, act^courses.edit_course_content, course-v1^*, allow p, role^course_staff, act^courses.manage_library_updates, course-v1^*, allow p, role^course_staff, act^courses.manage_course_updates, course-v1^*, allow @@ -142,6 +154,10 @@ p, role^course_admin, act^courses.view_grading_settings, course-v1^*, allow p, role^course_admin, act^courses.view_checklists, course-v1^*, allow p, role^course_admin, act^courses.view_course_team, course-v1^*, allow p, role^course_admin, act^courses.view_schedule_and_details, course-v1^*, allow +p, role^course_admin, act^courses.view_advanced_settings, course-v1^*, allow +p, role^course_admin, act^courses.view_certificates, course-v1^*, allow +p, role^course_admin, act^courses.view_group_configurations, course-v1^*, allow +p, role^course_admin, act^courses.view_library_updates, course-v1^*, allow p, role^course_admin, act^courses.edit_course_content, course-v1^*, allow p, role^course_admin, act^courses.manage_library_updates, course-v1^*, allow p, role^course_admin, act^courses.manage_course_updates, course-v1^*, allow diff --git a/openedx_authz/tests/api/test_roles.py b/openedx_authz/tests/api/test_roles.py index 9fef15eb..c7310a84 100644 --- a/openedx_authz/tests/api/test_roles.py +++ b/openedx_authz/tests/api/test_roles.py @@ -350,6 +350,17 @@ def setUpClass(cls): "role_name": roles.LIBRARY_CONTRIBUTOR.external_key, "scope_name": "lib:Org6:project_zeta", }, + # Course editor and auditor assignments for view/manage permission testing + { + "subject_name": "editor_user", + "role_name": roles.COURSE_EDITOR.external_key, + "scope_name": "course-v1:TestOrg+TestCourse+2024_T1", + }, + { + "subject_name": "auditor_user", + "role_name": roles.COURSE_AUDITOR.external_key, + "scope_name": "course-v1:TestOrg+TestCourse+2024_T1", + }, ] cls._assign_roles_to_users(assignments=assignments) diff --git a/openedx_authz/tests/api/test_users.py b/openedx_authz/tests/api/test_users.py index 9bb236f6..278a0063 100644 --- a/openedx_authz/tests/api/test_users.py +++ b/openedx_authz/tests/api/test_users.py @@ -580,6 +580,43 @@ class TestUserPermissions(UserAssignmentsSetupMixin): "course-v1:TestOrg+TestCourse+2024_T3", True, ), + # View/manage permission split: editor and auditor should NOT get manage permissions + ( + "editor_user", + permissions.COURSES_MANAGE_ADVANCED_SETTINGS.identifier, + "course-v1:TestOrg+TestCourse+2024_T1", + False, + ), + ( + "auditor_user", + permissions.COURSES_MANAGE_ADVANCED_SETTINGS.identifier, + "course-v1:TestOrg+TestCourse+2024_T1", + False, + ), + ( + "editor_user", + permissions.COURSES_MANAGE_CERTIFICATES.identifier, + "course-v1:TestOrg+TestCourse+2024_T1", + False, + ), + ( + "auditor_user", + permissions.COURSES_MANAGE_CERTIFICATES.identifier, + "course-v1:TestOrg+TestCourse+2024_T1", + False, + ), + ( + "auditor_user", + permissions.COURSES_MANAGE_GROUP_CONFIGURATIONS.identifier, + "course-v1:TestOrg+TestCourse+2024_T1", + False, + ), + ( + "auditor_user", + permissions.COURSES_MANAGE_LIBRARY_UPDATES.identifier, + "course-v1:TestOrg+TestCourse+2024_T1", + False, + ), # Library permissions ("alice", permissions.DELETE_LIBRARY.identifier, "lib:Org1:math_101", True), ("bob", permissions.PUBLISH_LIBRARY_CONTENT.identifier, "lib:Org1:history_201", True), diff --git a/openedx_authz/tests/test_engine_utils.py b/openedx_authz/tests/test_engine_utils.py index 5315c97f..87b790ba 100644 --- a/openedx_authz/tests/test_engine_utils.py +++ b/openedx_authz/tests/test_engine_utils.py @@ -76,10 +76,10 @@ def test_migrate_all_file_policies_to_database(self): Expected Result: - All policies from the file are loaded into the database - - The file contains 116 regular policies (p rules) + - The file contains 132 regular policies (p rules) - Policy content matches expected file content """ - expected_policy_count = 116 + expected_policy_count = 132 migrate_policy_between_enforcers(self.source_enforcer, self.target_enforcer) self.target_enforcer.load_policy() @@ -208,7 +208,7 @@ def test_migrate_complete_file_contents(self): """Test that all policy types from the file are migrated correctly. Expected Result: - - All regular policies (p) are migrated (116 rules) + - All regular policies (p) are migrated (132 rules) - No role assignments (g) - these come from database - All action inheritance rules (g2) are migrated (10 rules) """ @@ -216,8 +216,8 @@ def test_migrate_complete_file_contents(self): self.assertEqual( len(self.target_enforcer.get_policy()), - 116, - "Should have 116 regular policies from file", + 132, + "Should have 132 regular policies from file", ) self.assertEqual( len(self.target_enforcer.get_grouping_policy()), @@ -250,8 +250,8 @@ def test_migrate_partial_duplicates(self): target_policies = self.target_enforcer.get_policy() self.assertEqual( len(target_policies), - 116, - "Should have 116 policies total, with no duplicates", + 132, + "Should have 132 policies total, with no duplicates", ) duplicates = CasbinRule.objects.values("v0", "v1", "v2").annotate(total=Count("*")).filter(total__gt=1) @@ -346,7 +346,7 @@ def test_migrate_preserves_existing_db_policies(self): migrate_policy_between_enforcers(self.source_enforcer, self.target_enforcer) target_policies = self.target_enforcer.get_policy() - self.assertEqual(len(target_policies), 117, "Should have 116 file policies + 1 custom policy") + self.assertEqual(len(target_policies), 133, "Should have 132 file policies + 1 custom policy") self.assertIn(custom_policy, target_policies, "Custom database policy should be preserved") def test_migrate_preserves_user_role_assignments_in_db(self): @@ -382,4 +382,4 @@ def test_migrate_preserves_user_role_assignments_in_db(self): ) target_policies = self.target_enforcer.get_policy() - self.assertEqual(len(target_policies), 116, "All 116 policies from file should be loaded") + self.assertEqual(len(target_policies), 132, "All 132 policies from file should be loaded")