From 51a0b002003e7761f3b71f6276a8bd993cb312a4 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 16:32:06 +0200 Subject: [PATCH 01/11] refactor: organize built-in definitions into applets --- CHANGELOG.rst | 2 + pyproject.toml | 2 +- src/openedx_authz/applets/__init__.py | 1 + .../applets/content_libraries/README.rst | 11 + .../applets/content_libraries/__init__.py | 1 + .../applets/content_libraries/constants.py | 54 + .../applets/content_libraries/roles.py | 60 + .../schema/permissions.yaml} | 0 .../content_libraries/schema/roles.yaml} | 0 .../applets/course_authoring/README.rst | 10 + .../applets/course_authoring/__init__.py | 1 + .../applets/course_authoring/constants.py | 207 +++ .../course_authoring/schema/permissions.yaml} | 69 +- .../schema/role_extensions.yaml} | 82 +- .../applets/course_team_roles/README.rst | 12 + .../applets/course_team_roles/__init__.py | 1 + .../applets/course_team_roles/constants.py | 37 + .../applets/course_team_roles/roles.py | 209 +++ .../schema/legacy_permissions.yaml | 50 + .../schema/role_extensions.yaml | 19 + .../course_team_roles/schema/roles.yaml | 57 + src/openedx_authz/authz/__init__.py | 15 +- src/openedx_authz/constants/permissions.py | 270 +--- src/openedx_authz/constants/roles.py | 221 +-- src/openedx_authz/engine/renderer.py | 12 +- src/openedx_authz/engine/schema/discovery.py | 17 +- src/openedx_authz/engine/schema/loading.py | 2 +- .../engine/schema/types/loading.py | 2 +- .../management/commands/load_authz_schema.py | 5 +- ..._move_builtin_schema_sources_to_applets.py | 110 ++ src/openedx_authz/tests/applets/__init__.py | 1 + .../applets/fixtures/compiled_schema.json | 1432 +++++++++++++++++ .../tests/applets/test_builtin_definitions.py | 77 + .../tests/authz/test_resources.py | 7 + src/openedx_authz/tests/schema/test_apply.py | 1 - .../tests/schema/test_discovery.py | 46 +- .../tests/schema/test_loading.py | 9 +- src/openedx_authz/tests/test_imports.py | 39 + src/openedx_authz/tests/test_migrations.py | 96 ++ 39 files changed, 2609 insertions(+), 638 deletions(-) create mode 100644 src/openedx_authz/applets/__init__.py create mode 100644 src/openedx_authz/applets/content_libraries/README.rst create mode 100644 src/openedx_authz/applets/content_libraries/__init__.py create mode 100644 src/openedx_authz/applets/content_libraries/constants.py create mode 100644 src/openedx_authz/applets/content_libraries/roles.py rename src/openedx_authz/{authz/schema/library_permissions.yaml => applets/content_libraries/schema/permissions.yaml} (100%) rename src/openedx_authz/{authz/schema/library_roles.yaml => applets/content_libraries/schema/roles.yaml} (100%) create mode 100644 src/openedx_authz/applets/course_authoring/README.rst create mode 100644 src/openedx_authz/applets/course_authoring/__init__.py create mode 100644 src/openedx_authz/applets/course_authoring/constants.py rename src/openedx_authz/{authz/schema/course_permissions.yaml => applets/course_authoring/schema/permissions.yaml} (83%) rename src/openedx_authz/{authz/schema/course_roles.yaml => applets/course_authoring/schema/role_extensions.yaml} (61%) create mode 100644 src/openedx_authz/applets/course_team_roles/README.rst create mode 100644 src/openedx_authz/applets/course_team_roles/__init__.py create mode 100644 src/openedx_authz/applets/course_team_roles/constants.py create mode 100644 src/openedx_authz/applets/course_team_roles/roles.py create mode 100644 src/openedx_authz/applets/course_team_roles/schema/legacy_permissions.yaml create mode 100644 src/openedx_authz/applets/course_team_roles/schema/role_extensions.yaml create mode 100644 src/openedx_authz/applets/course_team_roles/schema/roles.yaml create mode 100644 src/openedx_authz/migrations/0012_move_builtin_schema_sources_to_applets.py create mode 100644 src/openedx_authz/tests/applets/__init__.py create mode 100644 src/openedx_authz/tests/applets/fixtures/compiled_schema.json create mode 100644 src/openedx_authz/tests/applets/test_builtin_definitions.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index de7ea543..3e19dab0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -56,6 +56,8 @@ Changed ======== * Static roles may declare an empty permission list when ``role_extensions`` contribute their permissions. Permission lists used by extension edits remain non-empty. +* Built-in Content Library and course authorization definitions now live in capability applets, + while the existing public imports and compiled authorization schema remain unchanged. Fixed ===== diff --git a/pyproject.toml b/pyproject.toml index 93aa8263..c237b2f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ openedx_authz = "openedx_authz.apps:OpenedxAuthzConfig" # Static authorization schema resources contributed by this package (ADR 0019). # openedx-authz is a schema provider like any other distribution; the callable -# returns resource paths relative to the openedx_authz.authz module. +# returns package-anchored resource paths for the built-in applets. [project.entry-points."authz.schema"] openedx_authz = "openedx_authz.authz:get_schema_resources" diff --git a/src/openedx_authz/applets/__init__.py b/src/openedx_authz/applets/__init__.py new file mode 100644 index 00000000..478a47fd --- /dev/null +++ b/src/openedx_authz/applets/__init__.py @@ -0,0 +1 @@ +"""Built-in Open edX authorization applets.""" diff --git a/src/openedx_authz/applets/content_libraries/README.rst b/src/openedx_authz/applets/content_libraries/README.rst new file mode 100644 index 00000000..4df6c4ea --- /dev/null +++ b/src/openedx_authz/applets/content_libraries/README.rst @@ -0,0 +1,11 @@ +Content Libraries +################# + +This applet owns the built-in roles and permissions for Content Libraries. Its +``schema`` directory is loaded through the package's existing ``authz.schema`` +entry point, while ``constants.py`` contains its permission objects and +``roles.py`` contains its role objects. + +The applet is an internal package. Existing consumers should continue importing +Library constants from ``openedx_authz.constants.permissions`` and +``openedx_authz.constants.roles``, which re-export the objects defined here. diff --git a/src/openedx_authz/applets/content_libraries/__init__.py b/src/openedx_authz/applets/content_libraries/__init__.py new file mode 100644 index 00000000..ef484134 --- /dev/null +++ b/src/openedx_authz/applets/content_libraries/__init__.py @@ -0,0 +1 @@ +"""Content Library authorization definitions.""" diff --git a/src/openedx_authz/applets/content_libraries/constants.py b/src/openedx_authz/applets/content_libraries/constants.py new file mode 100644 index 00000000..69e7d5a0 --- /dev/null +++ b/src/openedx_authz/applets/content_libraries/constants.py @@ -0,0 +1,54 @@ +"""Content Library permission constants.""" + +from openedx_authz.data import ActionData, PermissionData + +__all__ = [ + "CONTENT_LIBRARIES_NAMESPACE", + "VIEW_LIBRARY", + "MANAGE_LIBRARY_TAGS", + "DELETE_LIBRARY", + "EDIT_LIBRARY_CONTENT", + "PUBLISH_LIBRARY_CONTENT", + "REUSE_LIBRARY_CONTENT", + "VIEW_LIBRARY_TEAM", + "MANAGE_LIBRARY_TEAM", + "CREATE_LIBRARY_COLLECTION", + "EDIT_LIBRARY_COLLECTION", + "DELETE_LIBRARY_COLLECTION", +] + +CONTENT_LIBRARIES_NAMESPACE = "content_libraries" + +VIEW_LIBRARY = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.view_library"), effect="allow" +) +MANAGE_LIBRARY_TAGS = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.manage_library_tags"), effect="allow" +) +DELETE_LIBRARY = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.delete_library"), effect="allow" +) +EDIT_LIBRARY_CONTENT = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.edit_library_content"), effect="allow" +) +PUBLISH_LIBRARY_CONTENT = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.publish_library_content"), effect="allow" +) +REUSE_LIBRARY_CONTENT = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.reuse_library_content"), effect="allow" +) +VIEW_LIBRARY_TEAM = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.view_library_team"), effect="allow" +) +MANAGE_LIBRARY_TEAM = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.manage_library_team"), effect="allow" +) +CREATE_LIBRARY_COLLECTION = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.create_library_collection"), effect="allow" +) +EDIT_LIBRARY_COLLECTION = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.edit_library_collection"), effect="allow" +) +DELETE_LIBRARY_COLLECTION = PermissionData( + action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.delete_library_collection"), effect="allow" +) diff --git a/src/openedx_authz/applets/content_libraries/roles.py b/src/openedx_authz/applets/content_libraries/roles.py new file mode 100644 index 00000000..3e3e3b28 --- /dev/null +++ b/src/openedx_authz/applets/content_libraries/roles.py @@ -0,0 +1,60 @@ +"""Content Library role constants.""" + +from openedx_authz.api.data import RoleData +from openedx_authz.applets.content_libraries import constants as permissions + +LIBRARY_ADMIN_PERMISSIONS = [ + permissions.VIEW_LIBRARY, + permissions.MANAGE_LIBRARY_TAGS, + permissions.DELETE_LIBRARY, + permissions.EDIT_LIBRARY_CONTENT, + permissions.PUBLISH_LIBRARY_CONTENT, + permissions.REUSE_LIBRARY_CONTENT, + permissions.VIEW_LIBRARY_TEAM, + permissions.MANAGE_LIBRARY_TEAM, + permissions.CREATE_LIBRARY_COLLECTION, + permissions.EDIT_LIBRARY_COLLECTION, + permissions.DELETE_LIBRARY_COLLECTION, +] +LIBRARY_AUTHOR_PERMISSIONS = [ + permissions.VIEW_LIBRARY, + permissions.MANAGE_LIBRARY_TAGS, + permissions.EDIT_LIBRARY_CONTENT, + permissions.PUBLISH_LIBRARY_CONTENT, + permissions.REUSE_LIBRARY_CONTENT, + permissions.VIEW_LIBRARY_TEAM, + permissions.CREATE_LIBRARY_COLLECTION, + permissions.EDIT_LIBRARY_COLLECTION, + permissions.DELETE_LIBRARY_COLLECTION, +] +LIBRARY_CONTRIBUTOR_PERMISSIONS = [ + permissions.VIEW_LIBRARY, + permissions.MANAGE_LIBRARY_TAGS, + permissions.EDIT_LIBRARY_CONTENT, + permissions.REUSE_LIBRARY_CONTENT, + permissions.VIEW_LIBRARY_TEAM, + permissions.CREATE_LIBRARY_COLLECTION, + permissions.EDIT_LIBRARY_COLLECTION, + permissions.DELETE_LIBRARY_COLLECTION, +] +LIBRARY_USER_PERMISSIONS = [ + permissions.VIEW_LIBRARY, + permissions.REUSE_LIBRARY_CONTENT, + permissions.VIEW_LIBRARY_TEAM, +] + +LIBRARY_ADMIN = RoleData(external_key="library_admin", permissions=LIBRARY_ADMIN_PERMISSIONS) +LIBRARY_AUTHOR = RoleData(external_key="library_author", permissions=LIBRARY_AUTHOR_PERMISSIONS) +LIBRARY_CONTRIBUTOR = RoleData(external_key="library_contributor", permissions=LIBRARY_CONTRIBUTOR_PERMISSIONS) +LIBRARY_USER = RoleData(external_key="library_user", permissions=LIBRARY_USER_PERMISSIONS) + +__all__ = [ + "LIBRARY_ADMIN", + "LIBRARY_ADMIN_PERMISSIONS", + "LIBRARY_AUTHOR", + "LIBRARY_AUTHOR_PERMISSIONS", + "LIBRARY_CONTRIBUTOR", + "LIBRARY_CONTRIBUTOR_PERMISSIONS", + "LIBRARY_USER", + "LIBRARY_USER_PERMISSIONS", +] diff --git a/src/openedx_authz/authz/schema/library_permissions.yaml b/src/openedx_authz/applets/content_libraries/schema/permissions.yaml similarity index 100% rename from src/openedx_authz/authz/schema/library_permissions.yaml rename to src/openedx_authz/applets/content_libraries/schema/permissions.yaml diff --git a/src/openedx_authz/authz/schema/library_roles.yaml b/src/openedx_authz/applets/content_libraries/schema/roles.yaml similarity index 100% rename from src/openedx_authz/authz/schema/library_roles.yaml rename to src/openedx_authz/applets/content_libraries/schema/roles.yaml diff --git a/src/openedx_authz/applets/course_authoring/README.rst b/src/openedx_authz/applets/course_authoring/README.rst new file mode 100644 index 00000000..a2c9b107 --- /dev/null +++ b/src/openedx_authz/applets/course_authoring/README.rst @@ -0,0 +1,10 @@ +Course Authoring +################ + +This applet owns the existing built-in permissions for authoring, configuring, +and managing the team of a course. It adds those permissions to the shared +course-team roles through role extensions, while the role identifiers and +metadata remain in ``course_team_roles``. + +The applet is an internal package. Existing consumers should continue importing +permission and role constants from ``openedx_authz.constants``. diff --git a/src/openedx_authz/applets/course_authoring/__init__.py b/src/openedx_authz/applets/course_authoring/__init__.py new file mode 100644 index 00000000..ee884ed0 --- /dev/null +++ b/src/openedx_authz/applets/course_authoring/__init__.py @@ -0,0 +1 @@ +"""Course Authoring authorization definitions.""" diff --git a/src/openedx_authz/applets/course_authoring/constants.py b/src/openedx_authz/applets/course_authoring/constants.py new file mode 100644 index 00000000..2d5fce92 --- /dev/null +++ b/src/openedx_authz/applets/course_authoring/constants.py @@ -0,0 +1,207 @@ +"""Course Authoring permission constants and course-role contributions.""" + +from openedx_authz.data import ActionData, PermissionData + +__all__ = [ + "COURSES_NAMESPACE", + "COURSES_VIEW_COURSE", + "COURSES_VIEW_COURSE_TEAM", + "COURSES_MANAGE_COURSE_TEAM", + "COURSES_CREATE_COURSE", + "COURSES_EDIT_COURSE_CONTENT", + "COURSES_PUBLISH_COURSE_CONTENT", + "COURSES_VIEW_LIBRARY_UPDATES", + "COURSES_MANAGE_LIBRARY_UPDATES", + "COURSES_VIEW_COURSE_UPDATES", + "COURSES_MANAGE_COURSE_UPDATES", + "COURSES_VIEW_PAGES_AND_RESOURCES", + "COURSES_MANAGE_PAGES_AND_RESOURCES", + "COURSES_VIEW_FILES", + "COURSES_CREATE_FILES", + "COURSES_DELETE_FILES", + "COURSES_EDIT_FILES", + "COURSES_VIEW_SCHEDULE_AND_DETAILS", + "COURSES_EDIT_SCHEDULE", + "COURSES_EDIT_DETAILS", + "COURSES_VIEW_GRADING_SETTINGS", + "COURSES_EDIT_GRADING_SETTINGS", + "COURSES_VIEW_GROUP_CONFIGURATIONS", + "COURSES_MANAGE_GROUP_CONFIGURATIONS", + "COURSES_VIEW_ADVANCED_SETTINGS", + "COURSES_MANAGE_ADVANCED_SETTINGS", + "COURSES_VIEW_CERTIFICATES", + "COURSES_MANAGE_CERTIFICATES", + "COURSES_IMPORT_COURSE", + "COURSES_EXPORT_COURSE", + "COURSES_EXPORT_TAGS", + "COURSES_VIEW_CHECKLISTS", + "COURSES_MANAGE_TAGS", + "COURSES_MANAGE_TAXONOMIES", +] + +COURSES_NAMESPACE = "courses" + +COURSES_VIEW_COURSE = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_course"), + effect="allow", +) + +COURSES_VIEW_COURSE_TEAM = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_course_team"), + effect="allow", +) + +COURSES_MANAGE_COURSE_TEAM = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_course_team"), + effect="allow", +) + +COURSES_CREATE_COURSE = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.create_course"), + effect="allow", +) + +COURSES_EDIT_COURSE_CONTENT = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_course_content"), + effect="allow", +) + +COURSES_PUBLISH_COURSE_CONTENT = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.publish_course_content"), + 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", +) + +COURSES_VIEW_COURSE_UPDATES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_course_updates"), + effect="allow", +) + +COURSES_MANAGE_COURSE_UPDATES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_course_updates"), + effect="allow", +) + +COURSES_VIEW_PAGES_AND_RESOURCES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_pages_and_resources"), + effect="allow", +) + +COURSES_MANAGE_PAGES_AND_RESOURCES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_pages_and_resources"), + effect="allow", +) + +COURSES_VIEW_FILES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_files"), + effect="allow", +) + +COURSES_CREATE_FILES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.create_files"), + effect="allow", +) + +COURSES_DELETE_FILES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.delete_files"), + effect="allow", +) + +COURSES_EDIT_FILES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_files"), + effect="allow", +) + +COURSES_VIEW_SCHEDULE_AND_DETAILS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_schedule_and_details"), + effect="allow", +) + +COURSES_EDIT_SCHEDULE = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_schedule"), + effect="allow", +) + +COURSES_EDIT_DETAILS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_details"), + effect="allow", +) + +COURSES_VIEW_GRADING_SETTINGS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_grading_settings"), + effect="allow", +) + +COURSES_EDIT_GRADING_SETTINGS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_grading_settings"), + 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", +) + +COURSES_IMPORT_COURSE = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.import_course"), + effect="allow", +) + +COURSES_EXPORT_COURSE = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.export_course"), + effect="allow", +) + +COURSES_EXPORT_TAGS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.export_tags"), + effect="allow", +) + +COURSES_VIEW_CHECKLISTS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_checklists"), + effect="allow", +) + +COURSES_MANAGE_TAGS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_tags"), + effect="allow", +) + +COURSES_MANAGE_TAXONOMIES = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_taxonomies"), + effect="allow", +) diff --git a/src/openedx_authz/authz/schema/course_permissions.yaml b/src/openedx_authz/applets/course_authoring/schema/permissions.yaml similarity index 83% rename from src/openedx_authz/authz/schema/course_permissions.yaml rename to src/openedx_authz/applets/course_authoring/schema/permissions.yaml index bf168b94..d01d558e 100644 --- a/src/openedx_authz/authz/schema/course_permissions.yaml +++ b/src/openedx_authz/applets/course_authoring/schema/permissions.yaml @@ -55,12 +55,6 @@ permission_categories: display_name: Other description: Additional permissions not included in other categories, such as viewing checklists. icon: DrawShapes - # Backend-only category for legacy compatibility actions that have no frontend - # display metadata. These are referenced by legacy roles in authz.policy. - - id: course_legacy - display_name: Legacy compatibility - description: Internal compatibility permissions that map legacy course roles to the authorization system. Not shown as individually assignable permissions. - icon: DrawShapes permissions: - namespace: courses @@ -207,6 +201,14 @@ permissions: scopes: - course-v1 icon: EditOutline + - namespace: courses + name: view_group_configurations + display_name: View group configurations + description: See the list of content groups and their configurations for this course. + category_id: course_team_group + scopes: + - course-v1 + icon: RemoveRedEye - namespace: courses name: view_course_team display_name: View course team @@ -223,14 +225,6 @@ permissions: scopes: - course-v1 icon: Settings - - namespace: courses - name: view_group_configurations - display_name: View group configurations - description: See the list of content groups and their configurations for this course. - category_id: course_team_group - scopes: - - course-v1 - icon: RemoveRedEye - namespace: courses name: manage_group_configurations display_name: Manage group configurations @@ -319,50 +313,3 @@ permissions: scopes: - course-v1 icon: LocalOffer - - # ------------------------------------------------------------ - # Legacy compatibility course permissions - # Used for code paths that have not been upgraded to the - # new openedx-authz permissions. - # ------------------------------------------------------------ - - - namespace: courses - name: legacy_instructor_role_permissions - display_name: Legacy instructor permissions - description: Compatibility action mapping the legacy course instructor role into the authorization system. - category_id: course_legacy - scopes: - - course-v1 - icon: DrawShapes - - namespace: courses - name: legacy_staff_role_permissions - display_name: Legacy staff permissions - description: Compatibility action mapping the legacy course staff role into the authorization system. - category_id: course_legacy - scopes: - - course-v1 - icon: DrawShapes - - namespace: courses - name: legacy_limited_staff_role_permissions - display_name: Legacy limited staff permissions - description: Compatibility action mapping the legacy course limited staff role into the authorization system. - category_id: course_legacy - scopes: - - course-v1 - icon: DrawShapes - - namespace: courses - name: legacy_data_researcher_permissions - display_name: Legacy data researcher permissions - description: Compatibility action mapping the legacy course data researcher role into the authorization system. - category_id: course_legacy - scopes: - - course-v1 - icon: DrawShapes - - namespace: courses - name: legacy_beta_tester_permissions - display_name: Legacy beta tester permissions - description: Compatibility action mapping the legacy course beta tester role into the authorization system. - category_id: course_legacy - scopes: - - course-v1 - icon: DrawShapes diff --git a/src/openedx_authz/authz/schema/course_roles.yaml b/src/openedx_authz/applets/course_authoring/schema/role_extensions.yaml similarity index 61% rename from src/openedx_authz/authz/schema/course_roles.yaml rename to src/openedx_authz/applets/course_authoring/schema/role_extensions.yaml index f070550a..6c5645b0 100644 --- a/src/openedx_authz/authz/schema/course_roles.yaml +++ b/src/openedx_authz/applets/course_authoring/schema/role_extensions.yaml @@ -1,28 +1,20 @@ schema_version: "1.0" priority: 100 -# Course role definitions for the openedx-authz -# authorization schema. - -roles: - - id: course_admin - display_name: Course Admin - description: Can manage the course team and all course settings. - scopes: - - course-v1 - permissions: - - courses.legacy_instructor_role_permissions +role_extensions: + - role_id: course_admin + add_permissions: - courses.view_course - courses.view_course_updates - courses.view_pages_and_resources - courses.view_files - courses.view_grading_settings - courses.view_checklists - - courses.view_course_team - courses.view_schedule_and_details - courses.view_advanced_settings - courses.view_certificates - courses.view_group_configurations + - courses.view_course_team - courses.view_library_updates - courses.edit_course_content - courses.manage_library_updates @@ -32,6 +24,7 @@ roles: - courses.edit_files - courses.edit_grading_settings - courses.manage_group_configurations + - courses.manage_course_team - courses.edit_details - courses.manage_tags - courses.publish_course_content @@ -42,27 +35,21 @@ roles: - courses.import_course - courses.export_course - courses.export_tags - - courses.manage_course_team - courses.manage_taxonomies - - id: course_staff - display_name: Course Staff - description: Can publish content and manage the course lifecycle in Studio. - scopes: - - course-v1 - permissions: - - courses.legacy_staff_role_permissions + - role_id: course_staff + add_permissions: - courses.view_course - courses.view_course_updates - courses.view_pages_and_resources - courses.view_files - courses.view_grading_settings - courses.view_checklists - - courses.view_course_team - courses.view_schedule_and_details - courses.view_advanced_settings - courses.view_certificates - courses.view_group_configurations + - courses.view_course_team - courses.view_library_updates - courses.edit_course_content - courses.manage_library_updates @@ -83,24 +70,19 @@ roles: - courses.export_course - courses.export_tags - - id: course_editor - display_name: Course Editor - description: Can create and edit course content, but cannot publish or change critical course settings. - scopes: - - course-v1 - hidden: true - permissions: + - role_id: course_editor + add_permissions: - courses.view_course - courses.view_course_updates - courses.view_pages_and_resources - courses.view_files - courses.view_grading_settings - courses.view_checklists - - courses.view_course_team - courses.view_schedule_and_details - courses.view_advanced_settings - courses.view_certificates - courses.view_group_configurations + - courses.view_course_team - courses.view_library_updates - courses.edit_course_content - courses.manage_library_updates @@ -113,53 +95,17 @@ roles: - courses.edit_details - courses.manage_tags - - id: course_auditor - display_name: Course Auditor - description: Can view course content and settings, but cannot make changes. - scopes: - - course-v1 - hidden: true - permissions: + - role_id: course_auditor + add_permissions: - courses.view_course - courses.view_course_updates - courses.view_pages_and_resources - courses.view_files - courses.view_grading_settings - courses.view_checklists - - courses.view_course_team - courses.view_schedule_and_details - courses.view_advanced_settings - courses.view_certificates - courses.view_group_configurations + - courses.view_course_team - courses.view_library_updates - - # ------------------------------------------------------------ - # Legacy course roles. - # Each grants a single legacy compatibility permission. - # ------------------------------------------------------------ - - id: course_limited_staff - display_name: Course Limited Staff (legacy) - description: Legacy course role retained for backward compatibility. Grants the legacy limited staff compatibility permission. - scopes: - - course-v1 - hidden: true - permissions: - - courses.legacy_limited_staff_role_permissions - - - id: course_data_researcher - display_name: Course Data Researcher (legacy) - description: Legacy course role retained for backward compatibility. Grants the legacy data researcher compatibility permission. - scopes: - - course-v1 - hidden: true - permissions: - - courses.legacy_data_researcher_permissions - - - id: course_beta_tester - display_name: Course Beta Tester (legacy) - description: Legacy course role retained for backward compatibility. Grants the legacy beta tester compatibility permission. - scopes: - - course-v1 - hidden: true - permissions: - - courses.legacy_beta_tester_permissions diff --git a/src/openedx_authz/applets/course_team_roles/README.rst b/src/openedx_authz/applets/course_team_roles/README.rst new file mode 100644 index 00000000..7c3afb4b --- /dev/null +++ b/src/openedx_authz/applets/course_team_roles/README.rst @@ -0,0 +1,12 @@ +Course-Team Roles +################# + +This applet owns the identifiers and metadata for roles held by members of a +course team, including the legacy compatibility roles. Their base definitions +have empty permission lists because the applets that own each permission add it +through role extensions. + +The legacy compatibility permissions also live here because they connect these +roles to the previous course-role system. ``roles.py`` composes the complete +role constants from each applet's permission contributions, while existing +consumers can continue importing them from ``openedx_authz.constants``. diff --git a/src/openedx_authz/applets/course_team_roles/__init__.py b/src/openedx_authz/applets/course_team_roles/__init__.py new file mode 100644 index 00000000..04a6c49a --- /dev/null +++ b/src/openedx_authz/applets/course_team_roles/__init__.py @@ -0,0 +1 @@ +"""Shared course-team role definitions.""" diff --git a/src/openedx_authz/applets/course_team_roles/constants.py b/src/openedx_authz/applets/course_team_roles/constants.py new file mode 100644 index 00000000..77139a31 --- /dev/null +++ b/src/openedx_authz/applets/course_team_roles/constants.py @@ -0,0 +1,37 @@ +"""Course-team role identifiers and legacy compatibility permissions.""" + +from openedx_authz.data import ActionData, PermissionData + +__all__ = [ + "COURSES_LEGACY_BETA_TESTER_PERMISSIONS", + "COURSES_LEGACY_DATA_RESEARCHER_PERMISSIONS", + "COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS", + "COURSES_LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS", + "COURSES_LEGACY_STAFF_ROLE_PERMISSIONS", +] + +COURSES_NAMESPACE = "courses" + +COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_instructor_role_permissions"), effect="allow" +) +COURSES_LEGACY_STAFF_ROLE_PERMISSIONS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_staff_role_permissions"), effect="allow" +) +COURSES_LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_limited_staff_role_permissions"), effect="allow" +) +COURSES_LEGACY_DATA_RESEARCHER_PERMISSIONS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_data_researcher_permissions"), effect="allow" +) +COURSES_LEGACY_BETA_TESTER_PERMISSIONS = PermissionData( + action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_beta_tester_permissions"), effect="allow" +) + +COURSE_ADMIN_ID = "course_admin" +COURSE_STAFF_ID = "course_staff" +COURSE_EDITOR_ID = "course_editor" +COURSE_AUDITOR_ID = "course_auditor" +COURSE_LIMITED_STAFF_ID = "course_limited_staff" +COURSE_DATA_RESEARCHER_ID = "course_data_researcher" +COURSE_BETA_TESTER_ID = "course_beta_tester" diff --git a/src/openedx_authz/applets/course_team_roles/roles.py b/src/openedx_authz/applets/course_team_roles/roles.py new file mode 100644 index 00000000..6d0bbfe6 --- /dev/null +++ b/src/openedx_authz/applets/course_team_roles/roles.py @@ -0,0 +1,209 @@ +""" +Default roles and their associated permissions. +""" + +from openedx_authz.api.data import RoleData +from openedx_authz.applets.course_authoring import constants as course_authoring +from openedx_authz.applets.course_team_roles.constants import ( + COURSE_ADMIN_ID, + COURSE_AUDITOR_ID, + COURSE_BETA_TESTER_ID, + COURSE_DATA_RESEARCHER_ID, + COURSE_EDITOR_ID, + COURSE_LIMITED_STAFF_ID, + COURSE_STAFF_ID, + COURSES_LEGACY_BETA_TESTER_PERMISSIONS, + COURSES_LEGACY_DATA_RESEARCHER_PERMISSIONS, + COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS, + COURSES_LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS, + COURSES_LEGACY_STAFF_ROLE_PERMISSIONS, +) + +# Course Roles and Permissions + +COURSE_AUDITOR_PERMISSIONS = [ + course_authoring.COURSES_VIEW_COURSE, + course_authoring.COURSES_VIEW_COURSE_UPDATES, + course_authoring.COURSES_VIEW_PAGES_AND_RESOURCES, + course_authoring.COURSES_VIEW_FILES, + course_authoring.COURSES_VIEW_GRADING_SETTINGS, + course_authoring.COURSES_VIEW_CHECKLISTS, + course_authoring.COURSES_VIEW_COURSE_TEAM, + course_authoring.COURSES_VIEW_SCHEDULE_AND_DETAILS, + course_authoring.COURSES_VIEW_ADVANCED_SETTINGS, + course_authoring.COURSES_VIEW_CERTIFICATES, + course_authoring.COURSES_VIEW_GROUP_CONFIGURATIONS, + course_authoring.COURSES_VIEW_LIBRARY_UPDATES, +] + +COURSE_AUDITOR = RoleData( + external_key=COURSE_AUDITOR_ID, + permissions=COURSE_AUDITOR_PERMISSIONS, +) + +COURSE_EDITOR_PERMISSIONS = [ + course_authoring.COURSES_VIEW_COURSE, + course_authoring.COURSES_VIEW_COURSE_UPDATES, + course_authoring.COURSES_VIEW_PAGES_AND_RESOURCES, + course_authoring.COURSES_VIEW_FILES, + course_authoring.COURSES_VIEW_GRADING_SETTINGS, + course_authoring.COURSES_VIEW_CHECKLISTS, + course_authoring.COURSES_VIEW_COURSE_TEAM, + course_authoring.COURSES_VIEW_SCHEDULE_AND_DETAILS, + course_authoring.COURSES_VIEW_ADVANCED_SETTINGS, + course_authoring.COURSES_VIEW_CERTIFICATES, + course_authoring.COURSES_VIEW_GROUP_CONFIGURATIONS, + course_authoring.COURSES_VIEW_LIBRARY_UPDATES, + course_authoring.COURSES_EDIT_COURSE_CONTENT, + course_authoring.COURSES_MANAGE_LIBRARY_UPDATES, + course_authoring.COURSES_MANAGE_COURSE_UPDATES, + course_authoring.COURSES_MANAGE_PAGES_AND_RESOURCES, + course_authoring.COURSES_CREATE_FILES, + course_authoring.COURSES_EDIT_FILES, + course_authoring.COURSES_EDIT_GRADING_SETTINGS, + course_authoring.COURSES_MANAGE_GROUP_CONFIGURATIONS, + course_authoring.COURSES_EDIT_DETAILS, + course_authoring.COURSES_MANAGE_TAGS, +] + +COURSE_EDITOR = RoleData(external_key=COURSE_EDITOR_ID, permissions=COURSE_EDITOR_PERMISSIONS) + +COURSE_ADMIN_PERMISSIONS = [ + COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS, + course_authoring.COURSES_VIEW_COURSE, + course_authoring.COURSES_VIEW_COURSE_UPDATES, + course_authoring.COURSES_VIEW_PAGES_AND_RESOURCES, + course_authoring.COURSES_VIEW_FILES, + course_authoring.COURSES_VIEW_GRADING_SETTINGS, + course_authoring.COURSES_VIEW_CHECKLISTS, + course_authoring.COURSES_VIEW_COURSE_TEAM, + course_authoring.COURSES_VIEW_SCHEDULE_AND_DETAILS, + course_authoring.COURSES_VIEW_ADVANCED_SETTINGS, + course_authoring.COURSES_VIEW_CERTIFICATES, + course_authoring.COURSES_VIEW_GROUP_CONFIGURATIONS, + course_authoring.COURSES_VIEW_LIBRARY_UPDATES, + course_authoring.COURSES_EDIT_COURSE_CONTENT, + course_authoring.COURSES_MANAGE_LIBRARY_UPDATES, + course_authoring.COURSES_MANAGE_COURSE_UPDATES, + course_authoring.COURSES_MANAGE_PAGES_AND_RESOURCES, + course_authoring.COURSES_CREATE_FILES, + course_authoring.COURSES_EDIT_FILES, + course_authoring.COURSES_EDIT_GRADING_SETTINGS, + course_authoring.COURSES_MANAGE_GROUP_CONFIGURATIONS, + course_authoring.COURSES_EDIT_DETAILS, + course_authoring.COURSES_MANAGE_TAGS, + course_authoring.COURSES_PUBLISH_COURSE_CONTENT, + course_authoring.COURSES_DELETE_FILES, + course_authoring.COURSES_EDIT_SCHEDULE, + course_authoring.COURSES_MANAGE_ADVANCED_SETTINGS, + course_authoring.COURSES_MANAGE_CERTIFICATES, + course_authoring.COURSES_IMPORT_COURSE, + course_authoring.COURSES_EXPORT_COURSE, + course_authoring.COURSES_EXPORT_TAGS, + course_authoring.COURSES_MANAGE_COURSE_TEAM, + course_authoring.COURSES_MANAGE_TAXONOMIES, +] + +COURSE_ADMIN = RoleData(external_key=COURSE_ADMIN_ID, permissions=COURSE_ADMIN_PERMISSIONS) + +COURSE_STAFF_PERMISSIONS = [ + COURSES_LEGACY_STAFF_ROLE_PERMISSIONS, + course_authoring.COURSES_VIEW_COURSE, + course_authoring.COURSES_VIEW_COURSE_UPDATES, + course_authoring.COURSES_VIEW_PAGES_AND_RESOURCES, + course_authoring.COURSES_VIEW_FILES, + course_authoring.COURSES_VIEW_GRADING_SETTINGS, + course_authoring.COURSES_VIEW_CHECKLISTS, + course_authoring.COURSES_VIEW_COURSE_TEAM, + course_authoring.COURSES_VIEW_SCHEDULE_AND_DETAILS, + course_authoring.COURSES_VIEW_ADVANCED_SETTINGS, + course_authoring.COURSES_VIEW_CERTIFICATES, + course_authoring.COURSES_VIEW_GROUP_CONFIGURATIONS, + course_authoring.COURSES_VIEW_LIBRARY_UPDATES, + course_authoring.COURSES_EDIT_COURSE_CONTENT, + course_authoring.COURSES_MANAGE_LIBRARY_UPDATES, + course_authoring.COURSES_MANAGE_COURSE_UPDATES, + course_authoring.COURSES_MANAGE_PAGES_AND_RESOURCES, + course_authoring.COURSES_CREATE_FILES, + course_authoring.COURSES_EDIT_FILES, + course_authoring.COURSES_EDIT_GRADING_SETTINGS, + course_authoring.COURSES_MANAGE_GROUP_CONFIGURATIONS, + course_authoring.COURSES_EDIT_DETAILS, + course_authoring.COURSES_MANAGE_TAGS, + course_authoring.COURSES_PUBLISH_COURSE_CONTENT, + course_authoring.COURSES_DELETE_FILES, + course_authoring.COURSES_EDIT_SCHEDULE, + course_authoring.COURSES_MANAGE_ADVANCED_SETTINGS, + course_authoring.COURSES_MANAGE_CERTIFICATES, + course_authoring.COURSES_IMPORT_COURSE, + course_authoring.COURSES_EXPORT_COURSE, + course_authoring.COURSES_EXPORT_TAGS, +] + +COURSE_STAFF = RoleData(external_key=COURSE_STAFF_ID, permissions=COURSE_STAFF_PERMISSIONS) + +COURSE_LIMITED_STAFF_PERMISSIONS = [ + COURSES_LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS, +] + +COURSE_LIMITED_STAFF = RoleData( + external_key=COURSE_LIMITED_STAFF_ID, + permissions=COURSE_LIMITED_STAFF_PERMISSIONS, +) + +COURSE_DATA_RESEARCHER_PERMISSIONS = [ + COURSES_LEGACY_DATA_RESEARCHER_PERMISSIONS, +] + +COURSE_DATA_RESEARCHER = RoleData( + external_key=COURSE_DATA_RESEARCHER_ID, + permissions=COURSE_DATA_RESEARCHER_PERMISSIONS, +) + +COURSE_BETA_TESTER_PERMISSIONS = [ + COURSES_LEGACY_BETA_TESTER_PERMISSIONS, +] + +COURSE_BETA_TESTER = RoleData( + external_key=COURSE_BETA_TESTER_ID, + permissions=COURSE_BETA_TESTER_PERMISSIONS, +) + +# This is a known LMS-only permission, but doesn't actually grant anything yet. +# +# It is intended to be handled in the Willow time frame. +CCX_COACH_PERMISSIONS = [] +CCX_COACH = RoleData(external_key="ccx_coach", permissions=CCX_COACH_PERMISSIONS) + + +# Map of legacy course role names to their equivalent new roles +# This mapping must be unique in both directions, since it may be used as a reverse lookup (value → key). +# If multiple keys share the same value, it will lead to collisions. +LEGACY_COURSE_ROLE_EQUIVALENCES = { + "instructor": COURSE_ADMIN.external_key, + "staff": COURSE_STAFF.external_key, + "limited_staff": COURSE_LIMITED_STAFF.external_key, + "data_researcher": COURSE_DATA_RESEARCHER.external_key, + "beta_testers": COURSE_BETA_TESTER.external_key, + "ccx_coach": CCX_COACH.external_key, +} + +__all__ = [ + "CCX_COACH", + "CCX_COACH_PERMISSIONS", + "COURSE_ADMIN", + "COURSE_ADMIN_PERMISSIONS", + "COURSE_AUDITOR", + "COURSE_AUDITOR_PERMISSIONS", + "COURSE_BETA_TESTER", + "COURSE_BETA_TESTER_PERMISSIONS", + "COURSE_DATA_RESEARCHER", + "COURSE_DATA_RESEARCHER_PERMISSIONS", + "COURSE_EDITOR", + "COURSE_EDITOR_PERMISSIONS", + "COURSE_LIMITED_STAFF", + "COURSE_LIMITED_STAFF_PERMISSIONS", + "COURSE_STAFF", + "COURSE_STAFF_PERMISSIONS", + "LEGACY_COURSE_ROLE_EQUIVALENCES", +] diff --git a/src/openedx_authz/applets/course_team_roles/schema/legacy_permissions.yaml b/src/openedx_authz/applets/course_team_roles/schema/legacy_permissions.yaml new file mode 100644 index 00000000..7e3549a0 --- /dev/null +++ b/src/openedx_authz/applets/course_team_roles/schema/legacy_permissions.yaml @@ -0,0 +1,50 @@ +schema_version: "1.0" +priority: 100 + +permission_categories: + - id: course_legacy + display_name: Legacy compatibility + description: Internal compatibility permissions that map legacy course roles to the authorization system. Not shown as individually assignable permissions. + icon: DrawShapes + +permissions: + - namespace: courses + name: legacy_instructor_role_permissions + display_name: Legacy instructor permissions + description: Compatibility action mapping the legacy course instructor role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + - namespace: courses + name: legacy_staff_role_permissions + display_name: Legacy staff permissions + description: Compatibility action mapping the legacy course staff role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + - namespace: courses + name: legacy_limited_staff_role_permissions + display_name: Legacy limited staff permissions + description: Compatibility action mapping the legacy course limited staff role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + - namespace: courses + name: legacy_data_researcher_permissions + display_name: Legacy data researcher permissions + description: Compatibility action mapping the legacy course data researcher role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + - namespace: courses + name: legacy_beta_tester_permissions + display_name: Legacy beta tester permissions + description: Compatibility action mapping the legacy course beta tester role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes diff --git a/src/openedx_authz/applets/course_team_roles/schema/role_extensions.yaml b/src/openedx_authz/applets/course_team_roles/schema/role_extensions.yaml new file mode 100644 index 00000000..d07cea31 --- /dev/null +++ b/src/openedx_authz/applets/course_team_roles/schema/role_extensions.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" +priority: 100 + +role_extensions: + - role_id: course_admin + add_permissions: + - courses.legacy_instructor_role_permissions + - role_id: course_staff + add_permissions: + - courses.legacy_staff_role_permissions + - role_id: course_limited_staff + add_permissions: + - courses.legacy_limited_staff_role_permissions + - role_id: course_data_researcher + add_permissions: + - courses.legacy_data_researcher_permissions + - role_id: course_beta_tester + add_permissions: + - courses.legacy_beta_tester_permissions diff --git a/src/openedx_authz/applets/course_team_roles/schema/roles.yaml b/src/openedx_authz/applets/course_team_roles/schema/roles.yaml new file mode 100644 index 00000000..04d04455 --- /dev/null +++ b/src/openedx_authz/applets/course_team_roles/schema/roles.yaml @@ -0,0 +1,57 @@ +schema_version: "1.0" +priority: 100 + +roles: + - id: course_admin + display_name: Course Admin + description: Can manage the course team and all course settings. + scopes: + - course-v1 + permissions: [] + + - id: course_staff + display_name: Course Staff + description: Can publish content and manage the course lifecycle in Studio. + scopes: + - course-v1 + permissions: [] + + - id: course_editor + display_name: Course Editor + description: Can create and edit course content, but cannot publish or change critical course settings. + scopes: + - course-v1 + hidden: true + permissions: [] + + - id: course_auditor + display_name: Course Auditor + description: Can view course content and settings, but cannot make changes. + scopes: + - course-v1 + hidden: true + permissions: [] + + - id: course_limited_staff + display_name: Course Limited Staff (legacy) + description: Legacy course role retained for backward compatibility. Grants the legacy limited staff compatibility permission. + scopes: + - course-v1 + hidden: true + permissions: [] + + - id: course_data_researcher + display_name: Course Data Researcher (legacy) + description: Legacy course role retained for backward compatibility. Grants the legacy data researcher compatibility permission. + scopes: + - course-v1 + hidden: true + permissions: [] + + - id: course_beta_tester + display_name: Course Beta Tester (legacy) + description: Legacy course role retained for backward compatibility. Grants the legacy beta tester compatibility permission. + scopes: + - course-v1 + hidden: true + permissions: [] diff --git a/src/openedx_authz/authz/__init__.py b/src/openedx_authz/authz/__init__.py index 02944798..14ce435d 100644 --- a/src/openedx_authz/authz/__init__.py +++ b/src/openedx_authz/authz/__init__.py @@ -1,10 +1,9 @@ """openedx-authz's own static authorization schema resources. -This package ships the platform-default schema files under ``authz/schema`` and -exposes that directory through the ``authz.schema`` entry point (ADR 0019). -openedx-authz is a schema provider like any other distribution; its directory is -discovered the same way a third-party application's would be, and the loader -reads every ``.yaml`` file inside it. +This package exposes the built-in applet schema directories through the +``authz.schema`` entry point (ADR 0019). Openedx-authz is a schema provider like +any other distribution, and its directories are discovered the same way a +third-party application's would be. Register in setup.py / pyproject.toml:: @@ -20,7 +19,11 @@ # Directory paths (relative to an importable top-level package) that contain # this distribution's ``.yaml`` schema files. Per ADR 0019, providers return # directories, not individual files. -SCHEMA_DIRECTORIES: tuple[str, ...] = ("openedx_authz/authz/schema",) +SCHEMA_DIRECTORIES: tuple[str, ...] = ( + "openedx_authz/applets/content_libraries/schema", + "openedx_authz/applets/course_team_roles/schema", + "openedx_authz/applets/course_authoring/schema", +) def get_schema_resources() -> list[str]: diff --git a/src/openedx_authz/constants/permissions.py b/src/openedx_authz/constants/permissions.py index 4921d1c8..1c8a821b 100644 --- a/src/openedx_authz/constants/permissions.py +++ b/src/openedx_authz/constants/permissions.py @@ -1,254 +1,16 @@ -""" -Default permission constants. -""" - -from openedx_authz.data import ActionData, PermissionData - -# Content Library Permissions - -CONTENT_LIBRARIES_NAMESPACE = "content_libraries" - -VIEW_LIBRARY = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.view_library"), - effect="allow", -) -MANAGE_LIBRARY_TAGS = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.manage_library_tags"), - effect="allow", -) -DELETE_LIBRARY = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.delete_library"), - effect="allow", -) -EDIT_LIBRARY_CONTENT = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.edit_library_content"), - effect="allow", -) -PUBLISH_LIBRARY_CONTENT = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.publish_library_content"), - effect="allow", -) -REUSE_LIBRARY_CONTENT = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.reuse_library_content"), - effect="allow", -) -VIEW_LIBRARY_TEAM = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.view_library_team"), - effect="allow", -) -MANAGE_LIBRARY_TEAM = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.manage_library_team"), - effect="allow", -) - -CREATE_LIBRARY_COLLECTION = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.create_library_collection"), - effect="allow", -) -EDIT_LIBRARY_COLLECTION = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.edit_library_collection"), - effect="allow", -) -DELETE_LIBRARY_COLLECTION = PermissionData( - action=ActionData(external_key=f"{CONTENT_LIBRARIES_NAMESPACE}.delete_library_collection"), - effect="allow", -) - -# Course Permissions - -COURSES_NAMESPACE = "courses" - -COURSES_VIEW_COURSE = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_course"), - effect="allow", -) - -COURSES_CREATE_COURSE = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.create_course"), - effect="allow", -) - -COURSES_EDIT_COURSE_CONTENT = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_course_content"), - effect="allow", -) - -COURSES_PUBLISH_COURSE_CONTENT = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.publish_course_content"), - 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", -) - -COURSES_VIEW_COURSE_UPDATES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_course_updates"), - effect="allow", -) - -COURSES_MANAGE_COURSE_UPDATES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_course_updates"), - effect="allow", -) - -COURSES_VIEW_PAGES_AND_RESOURCES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_pages_and_resources"), - effect="allow", -) - -COURSES_MANAGE_PAGES_AND_RESOURCES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_pages_and_resources"), - effect="allow", -) - -COURSES_VIEW_FILES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_files"), - effect="allow", -) - -COURSES_CREATE_FILES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.create_files"), - effect="allow", -) - -COURSES_DELETE_FILES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.delete_files"), - effect="allow", -) - -COURSES_EDIT_FILES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_files"), - effect="allow", -) - -COURSES_VIEW_SCHEDULE_AND_DETAILS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_schedule_and_details"), - effect="allow", -) - -COURSES_EDIT_SCHEDULE = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_schedule"), - effect="allow", -) - -COURSES_EDIT_DETAILS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_details"), - effect="allow", -) - -COURSES_VIEW_GRADING_SETTINGS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_grading_settings"), - effect="allow", -) - -COURSES_EDIT_GRADING_SETTINGS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_grading_settings"), - effect="allow", -) - -COURSES_VIEW_COURSE_TEAM = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_course_team"), - effect="allow", -) - -COURSES_MANAGE_COURSE_TEAM = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_course_team"), - 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", -) - -COURSES_IMPORT_COURSE = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.import_course"), - effect="allow", -) - -COURSES_EXPORT_COURSE = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.export_course"), - effect="allow", -) - -COURSES_EXPORT_TAGS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.export_tags"), - effect="allow", -) - -COURSES_VIEW_CHECKLISTS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.view_checklists"), - effect="allow", -) - -COURSES_MANAGE_TAGS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_tags"), - effect="allow", -) - -COURSES_MANAGE_TAXONOMIES = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.manage_taxonomies"), - effect="allow", -) - -# Legacy Course permissions -# These permissions allow backwards compatibility with legacy code that depends on the old roles system -# These relate to legacy roles, if a openedx-authz role has one of these permissions, -# it will have the same permissions as the equivalent legacy roles on code that has not been updated to the new system. - -COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_instructor_role_permissions"), - effect="allow", -) - -COURSES_LEGACY_STAFF_ROLE_PERMISSIONS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_staff_role_permissions"), - effect="allow", -) - -COURSES_LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_limited_staff_role_permissions"), - effect="allow", -) - -COURSES_LEGACY_DATA_RESEARCHER_PERMISSIONS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_data_researcher_permissions"), - effect="allow", -) - -COURSES_LEGACY_BETA_TESTER_PERMISSIONS = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_beta_tester_permissions"), - effect="allow", -) +"""Compatibility exports for built-in permission constants.""" + +# pylint: disable=wildcard-import,unused-wildcard-import + +from openedx_authz.applets.content_libraries import constants as content_libraries +from openedx_authz.applets.content_libraries.constants import * # noqa: F403 +from openedx_authz.applets.course_authoring import constants as course_authoring +from openedx_authz.applets.course_authoring.constants import * # noqa: F403 +from openedx_authz.applets.course_team_roles import constants as course_team_roles +from openedx_authz.applets.course_team_roles.constants import * # noqa: F403 + +__all__ = [ + *content_libraries.__all__, + *course_authoring.__all__, + *course_team_roles.__all__, +] diff --git a/src/openedx_authz/constants/roles.py b/src/openedx_authz/constants/roles.py index 1d083f01..2845df34 100644 --- a/src/openedx_authz/constants/roles.py +++ b/src/openedx_authz/constants/roles.py @@ -1,216 +1,13 @@ -""" -Default roles and their associated permissions. -""" +"""Compatibility exports for built-in role constants.""" -from openedx_authz.api.data import RoleData -from openedx_authz.constants import permissions +# pylint: disable=wildcard-import,unused-wildcard-import -# Library Roles and Permissions +from openedx_authz.applets.content_libraries import roles as content_libraries +from openedx_authz.applets.content_libraries.roles import * # noqa: F403 +from openedx_authz.applets.course_team_roles import roles as course_team_roles +from openedx_authz.applets.course_team_roles.roles import * # noqa: F403 -# Define the associated permissions for each role - -LIBRARY_ADMIN_PERMISSIONS = [ - permissions.VIEW_LIBRARY, - permissions.MANAGE_LIBRARY_TAGS, - permissions.DELETE_LIBRARY, - permissions.EDIT_LIBRARY_CONTENT, - permissions.PUBLISH_LIBRARY_CONTENT, - permissions.REUSE_LIBRARY_CONTENT, - permissions.VIEW_LIBRARY_TEAM, - permissions.MANAGE_LIBRARY_TEAM, - permissions.CREATE_LIBRARY_COLLECTION, - permissions.EDIT_LIBRARY_COLLECTION, - permissions.DELETE_LIBRARY_COLLECTION, -] - -LIBRARY_AUTHOR_PERMISSIONS = [ - permissions.VIEW_LIBRARY, - permissions.MANAGE_LIBRARY_TAGS, - permissions.EDIT_LIBRARY_CONTENT, - permissions.PUBLISH_LIBRARY_CONTENT, - permissions.REUSE_LIBRARY_CONTENT, - permissions.VIEW_LIBRARY_TEAM, - permissions.CREATE_LIBRARY_COLLECTION, - permissions.EDIT_LIBRARY_COLLECTION, - permissions.DELETE_LIBRARY_COLLECTION, -] - -LIBRARY_CONTRIBUTOR_PERMISSIONS = [ - permissions.VIEW_LIBRARY, - permissions.MANAGE_LIBRARY_TAGS, - permissions.EDIT_LIBRARY_CONTENT, - permissions.REUSE_LIBRARY_CONTENT, - permissions.VIEW_LIBRARY_TEAM, - permissions.CREATE_LIBRARY_COLLECTION, - permissions.EDIT_LIBRARY_COLLECTION, - permissions.DELETE_LIBRARY_COLLECTION, -] - -LIBRARY_USER_PERMISSIONS = [ - permissions.VIEW_LIBRARY, - permissions.REUSE_LIBRARY_CONTENT, - permissions.VIEW_LIBRARY_TEAM, -] - -LIBRARY_ADMIN = RoleData(external_key="library_admin", permissions=LIBRARY_ADMIN_PERMISSIONS) -LIBRARY_AUTHOR = RoleData(external_key="library_author", permissions=LIBRARY_AUTHOR_PERMISSIONS) -LIBRARY_CONTRIBUTOR = RoleData(external_key="library_contributor", permissions=LIBRARY_CONTRIBUTOR_PERMISSIONS) -LIBRARY_USER = RoleData(external_key="library_user", permissions=LIBRARY_USER_PERMISSIONS) - - -# Course Roles and Permissions - -COURSE_AUDITOR_PERMISSIONS = [ - permissions.COURSES_VIEW_COURSE, - permissions.COURSES_VIEW_COURSE_UPDATES, - permissions.COURSES_VIEW_PAGES_AND_RESOURCES, - permissions.COURSES_VIEW_FILES, - permissions.COURSES_VIEW_GRADING_SETTINGS, - 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) - -COURSE_EDITOR_PERMISSIONS = [ - permissions.COURSES_VIEW_COURSE, - permissions.COURSES_VIEW_COURSE_UPDATES, - permissions.COURSES_VIEW_PAGES_AND_RESOURCES, - permissions.COURSES_VIEW_FILES, - permissions.COURSES_VIEW_GRADING_SETTINGS, - 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, - permissions.COURSES_MANAGE_PAGES_AND_RESOURCES, - permissions.COURSES_CREATE_FILES, - permissions.COURSES_EDIT_FILES, - permissions.COURSES_EDIT_GRADING_SETTINGS, - permissions.COURSES_MANAGE_GROUP_CONFIGURATIONS, - permissions.COURSES_EDIT_DETAILS, - permissions.COURSES_MANAGE_TAGS, -] - -COURSE_EDITOR = RoleData(external_key="course_editor", permissions=COURSE_EDITOR_PERMISSIONS) - -COURSE_ADMIN_PERMISSIONS = [ - permissions.COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS, - permissions.COURSES_VIEW_COURSE, - permissions.COURSES_VIEW_COURSE_UPDATES, - permissions.COURSES_VIEW_PAGES_AND_RESOURCES, - permissions.COURSES_VIEW_FILES, - permissions.COURSES_VIEW_GRADING_SETTINGS, - 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, - permissions.COURSES_MANAGE_PAGES_AND_RESOURCES, - permissions.COURSES_CREATE_FILES, - permissions.COURSES_EDIT_FILES, - permissions.COURSES_EDIT_GRADING_SETTINGS, - permissions.COURSES_MANAGE_GROUP_CONFIGURATIONS, - permissions.COURSES_EDIT_DETAILS, - permissions.COURSES_MANAGE_TAGS, - permissions.COURSES_PUBLISH_COURSE_CONTENT, - permissions.COURSES_DELETE_FILES, - permissions.COURSES_EDIT_SCHEDULE, - permissions.COURSES_MANAGE_ADVANCED_SETTINGS, - permissions.COURSES_MANAGE_CERTIFICATES, - permissions.COURSES_IMPORT_COURSE, - permissions.COURSES_EXPORT_COURSE, - permissions.COURSES_EXPORT_TAGS, - permissions.COURSES_MANAGE_COURSE_TEAM, - permissions.COURSES_MANAGE_TAXONOMIES, +__all__ = [ + *content_libraries.__all__, + *course_team_roles.__all__, ] - -COURSE_ADMIN = RoleData(external_key="course_admin", permissions=COURSE_ADMIN_PERMISSIONS) - -COURSE_STAFF_PERMISSIONS = [ - permissions.COURSES_LEGACY_STAFF_ROLE_PERMISSIONS, - permissions.COURSES_VIEW_COURSE, - permissions.COURSES_VIEW_COURSE_UPDATES, - permissions.COURSES_VIEW_PAGES_AND_RESOURCES, - permissions.COURSES_VIEW_FILES, - permissions.COURSES_VIEW_GRADING_SETTINGS, - 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, - permissions.COURSES_MANAGE_PAGES_AND_RESOURCES, - permissions.COURSES_CREATE_FILES, - permissions.COURSES_EDIT_FILES, - permissions.COURSES_EDIT_GRADING_SETTINGS, - permissions.COURSES_MANAGE_GROUP_CONFIGURATIONS, - permissions.COURSES_EDIT_DETAILS, - permissions.COURSES_MANAGE_TAGS, - permissions.COURSES_PUBLISH_COURSE_CONTENT, - permissions.COURSES_DELETE_FILES, - permissions.COURSES_EDIT_SCHEDULE, - permissions.COURSES_MANAGE_ADVANCED_SETTINGS, - permissions.COURSES_MANAGE_CERTIFICATES, - permissions.COURSES_IMPORT_COURSE, - permissions.COURSES_EXPORT_COURSE, - permissions.COURSES_EXPORT_TAGS, -] - -COURSE_STAFF = RoleData(external_key="course_staff", permissions=COURSE_STAFF_PERMISSIONS) - -COURSE_LIMITED_STAFF_PERMISSIONS = [ - permissions.COURSES_LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS, -] - -COURSE_LIMITED_STAFF = RoleData(external_key="course_limited_staff", permissions=COURSE_LIMITED_STAFF_PERMISSIONS) - -COURSE_DATA_RESEARCHER_PERMISSIONS = [ - permissions.COURSES_LEGACY_DATA_RESEARCHER_PERMISSIONS, -] - -COURSE_DATA_RESEARCHER = RoleData(external_key="course_data_researcher", permissions=COURSE_DATA_RESEARCHER_PERMISSIONS) - -COURSE_BETA_TESTER_PERMISSIONS = [ - permissions.COURSES_LEGACY_BETA_TESTER_PERMISSIONS, -] - -COURSE_BETA_TESTER = RoleData(external_key="course_beta_tester", permissions=COURSE_BETA_TESTER_PERMISSIONS) - -# This is a known LMS-only permission, but doesn't actually grant anything yet. -# -# It is intended to be handled in the Willow time frame. -CCX_COACH_PERMISSIONS = [] -CCX_COACH = RoleData(external_key="ccx_coach", permissions=CCX_COACH_PERMISSIONS) - - -# Map of legacy course role names to their equivalent new roles -# This mapping must be unique in both directions, since it may be used as a reverse lookup (value → key). -# If multiple keys share the same value, it will lead to collisions. -LEGACY_COURSE_ROLE_EQUIVALENCES = { - "instructor": COURSE_ADMIN.external_key, - "staff": COURSE_STAFF.external_key, - "limited_staff": COURSE_LIMITED_STAFF.external_key, - "data_researcher": COURSE_DATA_RESEARCHER.external_key, - "beta_testers": COURSE_BETA_TESTER.external_key, - "ccx_coach": CCX_COACH.external_key, -} diff --git a/src/openedx_authz/engine/renderer.py b/src/openedx_authz/engine/renderer.py index c580914f..c95a96ee 100644 --- a/src/openedx_authz/engine/renderer.py +++ b/src/openedx_authz/engine/renderer.py @@ -587,8 +587,9 @@ def source_obj(record): ) category_objs[cid] = obj for record in compiled.sources: + source = source_obj(record) schema_models.AuthzCategorySource.objects.update_or_create( - category=obj, source=source_obj(record), defaults={"origin_kind": schema_models.OriginKind.BASE} + category=obj, source=source, defaults={"origin_kind": schema_models.OriginKind.BASE} ) # Permissions. @@ -608,8 +609,9 @@ def source_obj(record): ) permission_objs[pid] = obj for record in compiled.sources: + source = source_obj(record) schema_models.AuthzPermissionSource.objects.update_or_create( - permission=obj, source=source_obj(record), defaults={"origin_kind": schema_models.OriginKind.BASE} + permission=obj, source=source, defaults={"origin_kind": schema_models.OriginKind.BASE} ) # Roles. @@ -628,8 +630,9 @@ def source_obj(record): ) role_objs[rid] = obj for record in compiled.sources: + source = source_obj(record) schema_models.AuthzRoleSource.objects.update_or_create( - role=obj, source=source_obj(record), defaults={"origin_kind": schema_models.OriginKind.BASE} + role=obj, source=source, defaults={"origin_kind": schema_models.OriginKind.BASE} ) # Role-permission grants (one per rendered role/permission/scope triple). @@ -649,9 +652,10 @@ def source_obj(record): ) live_grant_ids.add(grant.pk) for rel in schema.role_permission_sources.get((rid, perm_id), []): + source = source_obj(rel.source) schema_models.AuthzRolePermissionSource.objects.update_or_create( role_permission=grant, - source=source_obj(rel.source), + source=source, defaults={"origin_kind": rel.origin_kind, "priority": rel.priority}, ) diff --git a/src/openedx_authz/engine/schema/discovery.py b/src/openedx_authz/engine/schema/discovery.py index 830bd817..efc6d8c0 100644 --- a/src/openedx_authz/engine/schema/discovery.py +++ b/src/openedx_authz/engine/schema/discovery.py @@ -4,8 +4,8 @@ every ``.yaml`` file inside them. Two contribution sources are merged: 1. The ``authz.schema`` entry-point group. Each registered callable returns - directory paths in the package-anchored format below (e.g. - openedx-authz's ``["openedx_authz/authz/schema"]``). + directory paths in the package-anchored format below (for example, + ``["openedx_authz/applets/content_libraries/schema"]``). 2. The ``OPENEDX_AUTHZ_SCHEMA_DIRECTORIES`` Django setting, a list of directory path strings in the same format. This lets operators and CI contribute directories without shipping a package entry point. @@ -14,8 +14,8 @@ absolute). Each is a package-anchored ``importlib.resources`` path: the first segment is an importable top-level package (the *anchor*) and the remaining forward-slash segments name a resource container within it. For example -``"openedx_authz/authz/schema"`` anchors on the ``openedx_authz`` package and -addresses its ``authz/schema`` subdirectory. Resolving through +``"openedx_authz/applets/content_libraries/schema"`` anchors on the +``openedx_authz`` package and addresses the applet's ``schema`` subdirectory. Resolving through ``importlib.resources`` (rather than the filesystem) means discovery does not depend on virtualenv or container layout, and works even when the package is imported from a zip. If any provider raises, discovery @@ -69,9 +69,10 @@ class DiscoveredResource: package: Importable top-level package used as the ``importlib.resources`` anchor (e.g. ``openedx_authz``). resource_path: Path to the file within that anchor - (e.g. ``authz/schema/course_roles.yaml``). + (e.g. ``applets/course_team_roles/schema/roles.yaml``). module: Dotted path of the owning directory, used as the source-record - module and provenance identity (e.g. ``openedx_authz.authz.schema``). + module and provenance identity (e.g. + ``openedx_authz.applets.course_team_roles.schema``). origin: Which contribution route produced this resource, as an ``Origin`` member; diagnostics only. """ @@ -189,8 +190,8 @@ def _iter_directory(self, directory: str, *, origin: Origin) -> list[DiscoveredR The path's first segment is an importable top-level package used as the anchor; the remainder is a subdirectory within it. For example - ``"openedx_authz/authz/schema"`` anchors on ``openedx_authz`` and reads - the ``authz/schema`` subdirectory. + ``"openedx_authz/applets/content_libraries/schema"`` anchors on + ``openedx_authz`` and reads the applet's ``schema`` subdirectory. """ parts = [segment for segment in directory.strip("/").split("/") if segment] if not parts: diff --git a/src/openedx_authz/engine/schema/loading.py b/src/openedx_authz/engine/schema/loading.py index c5c70f68..7854b0dd 100644 --- a/src/openedx_authz/engine/schema/loading.py +++ b/src/openedx_authz/engine/schema/loading.py @@ -176,7 +176,7 @@ def _distribution_ships(distribution: str, installed_path: str) -> bool: ``Distribution.files`` lists the files the distribution installed, as anchor-relative ``PackagePath`` values (e.g. - ``openedx_authz/authz/schema/course_roles.yaml``). A resource belongs to + ``openedx_authz/applets/course_team_roles/schema/roles.yaml``). A resource belongs to the distribution when one of those paths ends with the resource's ``package/resource_path``. Metadata gaps (``files`` is ``None`` or the distribution is missing) mean "cannot confirm ownership", not an error. diff --git a/src/openedx_authz/engine/schema/types/loading.py b/src/openedx_authz/engine/schema/types/loading.py index baa7bc24..b91c1c5f 100644 --- a/src/openedx_authz/engine/schema/types/loading.py +++ b/src/openedx_authz/engine/schema/types/loading.py @@ -49,7 +49,7 @@ def source_id(self) -> str: """Stable, human-readable id. Combines the distribution with the module directory path and the file - name, e.g. ``"openedx-authz:openedx_authz/authz/schema/roles.yaml"``. + name, e.g. ``"openedx-authz:openedx_authz/applets/course_team_roles/schema/roles.yaml"``. ``module`` is the dotted path of the owning directory and ``resource_path`` is anchor-relative (so it may repeat the directory); only the file name diff --git a/src/openedx_authz/management/commands/load_authz_schema.py b/src/openedx_authz/management/commands/load_authz_schema.py index 0d2598d2..25b13dd5 100644 --- a/src/openedx_authz/management/commands/load_authz_schema.py +++ b/src/openedx_authz/management/commands/load_authz_schema.py @@ -10,7 +10,7 @@ python manage.py load_authz_schema --dry-run # report only, no writes python manage.py load_authz_schema --force # allow role removals python manage.py load_authz_schema \\ - --dir openedx_authz/authz/schema # explicit directory (CI/local) + --dir openedx_authz/applets/content_libraries/schema # explicit directory The command must run at a point where all contributing packages are installed and Django settings/DB are available (ADR 0018 / plugin timing constraint). @@ -53,7 +53,8 @@ def add_arguments(self, parser) -> None: help=( "Explicitly include a schema directory (repeatable), in addition to discovered " "entry points and settings. The loader reads every .yaml file in it. " - "Path format is 'top_level_package/sub/dir' (e.g. 'openedx_authz/authz/schema'). " + "Path format is 'top_level_package/sub/dir' " + "(e.g. 'openedx_authz/applets/content_libraries/schema'). " "Intended for CI and local development." ), ) diff --git a/src/openedx_authz/migrations/0012_move_builtin_schema_sources_to_applets.py b/src/openedx_authz/migrations/0012_move_builtin_schema_sources_to_applets.py new file mode 100644 index 00000000..01041369 --- /dev/null +++ b/src/openedx_authz/migrations/0012_move_builtin_schema_sources_to_applets.py @@ -0,0 +1,110 @@ +"""Move persisted built-in schema ownership from the shared module to applets. + +Released versions could load all built-in definitions from +``openedx_authz.authz.schema``, so installations that ran the schema loader may +have source links that still identify that shared module. The definitions now +live in applets, and this migration updates those persisted links so source +queries continue to report their current owners after an upgrade. + +Installations without the shared source need no repair. Existing applet links +are also preserved because they may have been created by a previous schema +load or a partially completed deployment. +""" + +from django.db import migrations + +OLD_MODULE = "openedx_authz.authz.schema" +APPLET_MODULES = { + "content_libraries": "openedx_authz.applets.content_libraries.schema", + "course_authoring": "openedx_authz.applets.course_authoring.schema", + "course_team_roles": "openedx_authz.applets.course_team_roles.schema", +} + + +def _category_owner(category_id): + """Return the applet that owns a built-in permission category.""" + if category_id.startswith("library"): + return "content_libraries" + if category_id == "course_legacy": + return "course_team_roles" + return "course_authoring" + + +def _permission_owner(namespace, name): + """Return the applet that owns a built-in permission.""" + if namespace == "content_libraries": + return "content_libraries" + if name.startswith("legacy_"): + return "course_team_roles" + return "course_authoring" + + +def _role_owner(role_id): + """Return the applet that owns a built-in role.""" + if role_id.startswith("library_"): + return "content_libraries" + return "course_team_roles" + + +def _get_applet_source(source, owner): + """Return the destination source for one applet, creating it when needed.""" + applet_source, _ = source.__class__.objects.get_or_create( + distribution=source.distribution, + module=APPLET_MODULES[owner], + defaults={ + "distribution_version": source.distribution_version, + "schema_version": source.schema_version, + }, + ) + return applet_source + + +def _move_link(link, source, link_model, owner_field, owner): + """Copy one source relationship to its applet source and remove the old link.""" + applet_source = _get_applet_source(source, owner) + link_model.objects.get_or_create( + **{owner_field: getattr(link, owner_field), "source": applet_source}, + defaults={"origin_kind": link.origin_kind, "priority": link.priority}, + ) + link.delete() + + +def move_builtin_schema_sources(apps, _schema_editor): + """Redistribute released built-in source links among their owning applets.""" + Source = apps.get_model("openedx_authz", "AuthzSchemaSource") + old_sources = Source.objects.filter(module=OLD_MODULE) + if not old_sources.exists(): + return + + CategorySource = apps.get_model("openedx_authz", "AuthzCategorySource") + PermissionSource = apps.get_model("openedx_authz", "AuthzPermissionSource") + RoleSource = apps.get_model("openedx_authz", "AuthzRoleSource") + RolePermissionSource = apps.get_model("openedx_authz", "AuthzRolePermissionSource") + + for source in old_sources: + for link in list(CategorySource.objects.filter(source=source).select_related("category")): + _move_link(link, source, CategorySource, "category", _category_owner(link.category.category_id)) + for link in list(PermissionSource.objects.filter(source=source).select_related("permission")): + owner = _permission_owner(link.permission.namespace, link.permission.name) + _move_link(link, source, PermissionSource, "permission", owner) + for link in list(RoleSource.objects.filter(source=source).select_related("role")): + _move_link(link, source, RoleSource, "role", _role_owner(link.role.role_id)) + for link in list( + RolePermissionSource.objects.filter(source=source).select_related("role_permission__permission") + ): + permission = link.role_permission.permission + owner = _permission_owner(permission.namespace, permission.name) + _move_link(link, source, RolePermissionSource, "role_permission", owner) + source.delete() + + +class Migration(migrations.Migration): + """Move persisted built-in schema ownership to the applet modules.""" + + dependencies = [ + ("openedx_authz", "0011_authz_schema_definitions"), + ] + + operations = [ + migrations.RunPython(move_builtin_schema_sources, migrations.RunPython.noop), + ] diff --git a/src/openedx_authz/tests/applets/__init__.py b/src/openedx_authz/tests/applets/__init__.py new file mode 100644 index 00000000..adfe9be0 --- /dev/null +++ b/src/openedx_authz/tests/applets/__init__.py @@ -0,0 +1 @@ +"""Tests for built-in authorization applets.""" diff --git a/src/openedx_authz/tests/applets/fixtures/compiled_schema.json b/src/openedx_authz/tests/applets/fixtures/compiled_schema.json new file mode 100644 index 00000000..f3288358 --- /dev/null +++ b/src/openedx_authz/tests/applets/fixtures/compiled_schema.json @@ -0,0 +1,1432 @@ +{ + "categories": { + "course_access_content": { + "description": "Permissions related to accessing the course and managing core course content, including creating, editing, and publishing materials.", + "display_name": "Course access & content", + "icon": "BookOpen", + "id": "course_access_content" + }, + "course_advanced_certificates": { + "description": "Permissions for managing advanced course settings and course certificates.", + "display_name": "Advanced & certificates", + "icon": "CheckCircle", + "id": "course_advanced_certificates" + }, + "course_files": { + "description": "Permissions for viewing and managing files and assets uploaded to the course.", + "display_name": "Files", + "icon": "Folder", + "id": "course_files" + }, + "course_grading": { + "description": "Permissions related to viewing and managing grading configuration and grading policies.", + "display_name": "Grading", + "icon": "Award", + "id": "course_grading" + }, + "course_import_export": { + "description": "Permissions for importing and exporting course content and related data.", + "display_name": "Import / export", + "icon": "Download", + "id": "course_import_export" + }, + "course_legacy": { + "description": "Internal compatibility permissions that map legacy course roles to the authorization system. Not shown as individually assignable permissions.", + "display_name": "Legacy compatibility", + "icon": "DrawShapes", + "id": "course_legacy" + }, + "course_library_updates": { + "description": "Permissions for reviewing and managing updates made to content libraries connected to the course.", + "display_name": "Library updates", + "icon": "LibraryBooks", + "id": "course_library_updates" + }, + "course_other": { + "description": "Additional permissions not included in other categories, such as viewing checklists.", + "display_name": "Other", + "icon": "DrawShapes", + "id": "course_other" + }, + "course_pages_resources": { + "description": "Permissions for viewing and managing course pages and additional learning resources.", + "display_name": "Pages & resources", + "icon": "Article", + "id": "course_pages_resources" + }, + "course_schedule_details": { + "description": "Permissions for viewing and editing the course schedule and course information.", + "display_name": "Schedule & details", + "icon": "Calendar", + "id": "course_schedule_details" + }, + "course_tags_taxonomies": { + "description": "Permissions for managing tags used to organize course content.", + "display_name": "Tags", + "icon": "LocalOffer", + "id": "course_tags_taxonomies" + }, + "course_team_group": { + "description": "Permissions for viewing and managing the course team, learner groups, and group configurations.", + "display_name": "Course team & groups", + "icon": "Group", + "id": "course_team_group" + }, + "course_updates_handouts": { + "description": "Permissions for viewing and managing course updates and handouts that are visible to learners.", + "display_name": "Course updates & handouts", + "icon": "Sync", + "id": "course_updates_handouts" + }, + "library": { + "description": "Permissions related to viewing, managing, and publishing the library structure and metadata.", + "display_name": "Library", + "icon": "CollectionsBookmark", + "id": "library" + }, + "library_collection": { + "description": "Permissions for creating and managing content collections within the library.", + "display_name": "Collection", + "icon": "AutoAwesomeMosaic", + "id": "library_collection" + }, + "library_content": { + "description": "Permissions for editing, publishing, and reusing content within the library.", + "display_name": "Content", + "icon": "Notes", + "id": "library_content" + }, + "library_team": { + "description": "Permissions for viewing and managing users who have access to the library.", + "display_name": "Team", + "icon": "Group", + "id": "library_team" + } + }, + "permissions": { + "content_libraries.create_library_collection": { + "category_id": "library_collection", + "description": "Create new collections to organize content within the library.", + "display_name": "Create", + "icon": "Plus", + "name": "create_library_collection", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.delete_library": { + "category_id": "library", + "description": "Allows users to delete the entire content library.", + "display_name": "Delete", + "icon": "Delete", + "name": "delete_library", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.delete_library_collection": { + "category_id": "library_collection", + "description": "Permanently remove collections from the library.", + "display_name": "Delete", + "icon": "Delete", + "name": "delete_library_collection", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.edit_library_collection": { + "category_id": "library_collection", + "description": "Update the name and contents of existing collections.", + "display_name": "Edit", + "icon": "EditOutline", + "name": "edit_library_collection", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.edit_library_content": { + "category_id": "library_content", + "description": "Create, edit, and delete content items in the library.", + "display_name": "Edit", + "icon": "EditOutline", + "name": "edit_library_content", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.manage_library_tags": { + "category_id": "library", + "description": "Create, edit, and delete tags on this library.", + "display_name": "Manage tags", + "icon": "Settings", + "name": "manage_library_tags", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.manage_library_team": { + "category_id": "library_team", + "description": "Add, change, or remove role assignments for this library from the Roles and Permissions console.", + "display_name": "Manage", + "icon": "Settings", + "name": "manage_library_team", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.publish_library_content": { + "category_id": "library_content", + "description": "Publish individual content items to make them available for reuse in courses.", + "display_name": "Publish", + "icon": "DownloadDone", + "name": "publish_library_content", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.reuse_library_content": { + "category_id": "library_content", + "description": "Add published content from this library to a course.", + "display_name": "Reuse", + "icon": "SpinnerIcon", + "name": "reuse_library_content", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.view_library": { + "category_id": "library", + "description": "See the library in Studio and access its content in read-only mode.", + "display_name": "View", + "icon": "RemoveRedEye", + "name": "view_library", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "content_libraries.view_library_team": { + "category_id": "library_team", + "description": "See the list of users with a role assigned to this library.", + "display_name": "View", + "icon": "RemoveRedEye", + "name": "view_library_team", + "namespace": "content_libraries", + "scopes": [ + "lib" + ] + }, + "courses.create_files": { + "category_id": "course_files", + "description": "Upload new files and assets to the course.", + "display_name": "Create files", + "icon": "Plus", + "name": "create_files", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.delete_files": { + "category_id": "course_files", + "description": "Permanently remove files and assets from the course.", + "display_name": "Delete files", + "icon": "Delete", + "name": "delete_files", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.edit_course_content": { + "category_id": "course_access_content", + "description": "Edit the course outline, units, and components.", + "display_name": "Edit course content", + "icon": "EditOutline", + "name": "edit_course_content", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.edit_details": { + "category_id": "course_schedule_details", + "description": "Update course information including the course summary, pacing, and prerequisites.", + "display_name": "Edit course details", + "icon": "EditOutline", + "name": "edit_details", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.edit_files": { + "category_id": "course_files", + "description": "Perform non-destructive actions on files, such as locking or unlocking them.", + "display_name": "Edit files", + "icon": "EditOutline", + "name": "edit_files", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.edit_grading_settings": { + "category_id": "course_grading", + "description": "Update the grading configuration for the course, including assignment types and grading scale.", + "display_name": "Edit grading settings", + "icon": "EditOutline", + "name": "edit_grading_settings", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.edit_schedule": { + "category_id": "course_schedule_details", + "description": "Update course start and end dates, enrollment dates, and pacing settings.", + "display_name": "Edit schedule", + "icon": "EditOutline", + "name": "edit_schedule", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.export_course": { + "category_id": "course_import_export", + "description": "Download the course content as a file for backup or reuse in another platform.", + "display_name": "Export course", + "icon": "Upload", + "name": "export_course", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.export_tags": { + "category_id": "course_import_export", + "description": "Download the tag data associated with this course.", + "display_name": "Export tags", + "icon": "Upload", + "name": "export_tags", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.import_course": { + "category_id": "course_import_export", + "description": "Import course content from a file. This is a high-privilege action that can overwrite most course content and settings.", + "display_name": "Import course", + "icon": "Download", + "name": "import_course", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.legacy_beta_tester_permissions": { + "category_id": "course_legacy", + "description": "Compatibility action mapping the legacy course beta tester role into the authorization system.", + "display_name": "Legacy beta tester permissions", + "icon": "DrawShapes", + "name": "legacy_beta_tester_permissions", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.legacy_data_researcher_permissions": { + "category_id": "course_legacy", + "description": "Compatibility action mapping the legacy course data researcher role into the authorization system.", + "display_name": "Legacy data researcher permissions", + "icon": "DrawShapes", + "name": "legacy_data_researcher_permissions", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.legacy_instructor_role_permissions": { + "category_id": "course_legacy", + "description": "Compatibility action mapping the legacy course instructor role into the authorization system.", + "display_name": "Legacy instructor permissions", + "icon": "DrawShapes", + "name": "legacy_instructor_role_permissions", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.legacy_limited_staff_role_permissions": { + "category_id": "course_legacy", + "description": "Compatibility action mapping the legacy course limited staff role into the authorization system.", + "display_name": "Legacy limited staff permissions", + "icon": "DrawShapes", + "name": "legacy_limited_staff_role_permissions", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.legacy_staff_role_permissions": { + "category_id": "course_legacy", + "description": "Compatibility action mapping the legacy course staff role into the authorization system.", + "display_name": "Legacy staff permissions", + "icon": "DrawShapes", + "name": "legacy_staff_role_permissions", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_advanced_settings": { + "category_id": "course_advanced_certificates", + "description": "Edit technical course configurations in the Advanced Settings page in Studio.", + "display_name": "Manage advanced settings", + "icon": "Settings", + "name": "manage_advanced_settings", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_certificates": { + "category_id": "course_advanced_certificates", + "description": "Create and edit course certificates, including certificate design and eligibility settings.", + "display_name": "Manage certificates", + "icon": "Settings", + "name": "manage_certificates", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_course_team": { + "category_id": "course_team_group", + "description": "Add, change, or remove role assignments for this course from the Roles and Permissions console.", + "display_name": "Manage course team", + "icon": "Settings", + "name": "manage_course_team", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_course_updates": { + "category_id": "course_updates_handouts", + "description": "Create, edit, and delete course announcements and handouts.", + "display_name": "Manage course updates", + "icon": "Settings", + "name": "manage_course_updates", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_group_configurations": { + "category_id": "course_team_group", + "description": "Create and manage content groups used to target course content to specific learners.", + "display_name": "Manage group configurations", + "icon": "Settings", + "name": "manage_group_configurations", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_library_updates": { + "category_id": "course_library_updates", + "description": "Accept or reject pending updates from content libraries linked to this course.", + "display_name": "Manage library updates", + "icon": "Checklist", + "name": "manage_library_updates", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_pages_and_resources": { + "category_id": "course_pages_resources", + "description": "Enable or disable course features such as Discussions, the Wiki, Notes, Calculator, and Live. Create and edit Textbooks and Custom pages, and manage their configurations.", + "display_name": "Manage pages & resources", + "icon": "Settings", + "name": "manage_pages_and_resources", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_tags": { + "category_id": "course_tags_taxonomies", + "description": "Create, edit, and delete tags on this course.", + "display_name": "Manage tags", + "icon": "Settings", + "name": "manage_tags", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.manage_taxonomies": { + "category_id": "course_tags_taxonomies", + "description": "Manage taxonomies associated with this course. (Granted to course_admin; not currently surfaced in the admin console.)", + "display_name": "Manage taxonomies", + "icon": "LocalOffer", + "name": "manage_taxonomies", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.publish_course_content": { + "category_id": "course_access_content", + "description": "Make course content visible to learners.", + "display_name": "Publish course content", + "icon": "DownloadDone", + "name": "publish_course_content", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_advanced_settings": { + "category_id": "course_advanced_certificates", + "description": "Access the Advanced Settings page in Studio. This covers a wide range of technical course configurations, including proctoring, timed exams, LTI tools, enrollment limits, and custom display options.", + "display_name": "View advanced settings", + "icon": "RemoveRedEye", + "name": "view_advanced_settings", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_certificates": { + "category_id": "course_advanced_certificates", + "description": "See the course certificate settings.", + "display_name": "View certificates", + "icon": "RemoveRedEye", + "name": "view_certificates", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_checklists": { + "category_id": "course_other", + "description": "See the course launch checklist in Studio.", + "display_name": "View checklists", + "icon": "RemoveRedEye", + "name": "view_checklists", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_course": { + "category_id": "course_access_content", + "description": "See the course in the Studio home and access the course outline in read-only mode. Includes the \"View Live\" option to preview the course as a learner in the LMS.", + "display_name": "View course", + "icon": "RemoveRedEye", + "name": "view_course", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_course_team": { + "category_id": "course_team_group", + "description": "See the list of users with a role assigned to this course.", + "display_name": "View course team", + "icon": "RemoveRedEye", + "name": "view_course_team", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_course_updates": { + "category_id": "course_updates_handouts", + "description": "See course announcements and handouts visible to learners.", + "display_name": "View course updates", + "icon": "RemoveRedEye", + "name": "view_course_updates", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_files": { + "category_id": "course_files", + "description": "See the list of files and assets uploaded to the course.", + "display_name": "View files", + "icon": "RemoveRedEye", + "name": "view_files", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_grading_settings": { + "category_id": "course_grading", + "description": "See the grading configuration for the course, including assignment types and grading scale.", + "display_name": "View grading settings", + "icon": "RemoveRedEye", + "name": "view_grading_settings", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_group_configurations": { + "category_id": "course_team_group", + "description": "See the list of content groups and their configurations for this course.", + "display_name": "View group configurations", + "icon": "RemoveRedEye", + "name": "view_group_configurations", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_library_updates": { + "category_id": "course_library_updates", + "description": "View pending updates from content libraries linked to this course.", + "display_name": "View library updates", + "icon": "RemoveRedEye", + "name": "view_library_updates", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_pages_and_resources": { + "category_id": "course_pages_resources", + "description": "See the Pages & Resources section in Studio.", + "display_name": "View pages & resources", + "icon": "RemoveRedEye", + "name": "view_pages_and_resources", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + }, + "courses.view_schedule_and_details": { + "category_id": "course_schedule_details", + "description": "See the course schedule (start and end dates, enrollment dates, and pacing settings) and course details (summary, pacing, and prerequisites).", + "display_name": "View schedule & details", + "icon": "RemoveRedEye", + "name": "view_schedule_and_details", + "namespace": "courses", + "scopes": [ + "course-v1" + ] + } + }, + "role_permission_pairs": [ + [ + "course_admin", + "courses.create_files" + ], + [ + "course_admin", + "courses.delete_files" + ], + [ + "course_admin", + "courses.edit_course_content" + ], + [ + "course_admin", + "courses.edit_details" + ], + [ + "course_admin", + "courses.edit_files" + ], + [ + "course_admin", + "courses.edit_grading_settings" + ], + [ + "course_admin", + "courses.edit_schedule" + ], + [ + "course_admin", + "courses.export_course" + ], + [ + "course_admin", + "courses.export_tags" + ], + [ + "course_admin", + "courses.import_course" + ], + [ + "course_admin", + "courses.legacy_instructor_role_permissions" + ], + [ + "course_admin", + "courses.manage_advanced_settings" + ], + [ + "course_admin", + "courses.manage_certificates" + ], + [ + "course_admin", + "courses.manage_course_team" + ], + [ + "course_admin", + "courses.manage_course_updates" + ], + [ + "course_admin", + "courses.manage_group_configurations" + ], + [ + "course_admin", + "courses.manage_library_updates" + ], + [ + "course_admin", + "courses.manage_pages_and_resources" + ], + [ + "course_admin", + "courses.manage_tags" + ], + [ + "course_admin", + "courses.manage_taxonomies" + ], + [ + "course_admin", + "courses.publish_course_content" + ], + [ + "course_admin", + "courses.view_advanced_settings" + ], + [ + "course_admin", + "courses.view_certificates" + ], + [ + "course_admin", + "courses.view_checklists" + ], + [ + "course_admin", + "courses.view_course" + ], + [ + "course_admin", + "courses.view_course_team" + ], + [ + "course_admin", + "courses.view_course_updates" + ], + [ + "course_admin", + "courses.view_files" + ], + [ + "course_admin", + "courses.view_grading_settings" + ], + [ + "course_admin", + "courses.view_group_configurations" + ], + [ + "course_admin", + "courses.view_library_updates" + ], + [ + "course_admin", + "courses.view_pages_and_resources" + ], + [ + "course_admin", + "courses.view_schedule_and_details" + ], + [ + "course_auditor", + "courses.view_advanced_settings" + ], + [ + "course_auditor", + "courses.view_certificates" + ], + [ + "course_auditor", + "courses.view_checklists" + ], + [ + "course_auditor", + "courses.view_course" + ], + [ + "course_auditor", + "courses.view_course_team" + ], + [ + "course_auditor", + "courses.view_course_updates" + ], + [ + "course_auditor", + "courses.view_files" + ], + [ + "course_auditor", + "courses.view_grading_settings" + ], + [ + "course_auditor", + "courses.view_group_configurations" + ], + [ + "course_auditor", + "courses.view_library_updates" + ], + [ + "course_auditor", + "courses.view_pages_and_resources" + ], + [ + "course_auditor", + "courses.view_schedule_and_details" + ], + [ + "course_beta_tester", + "courses.legacy_beta_tester_permissions" + ], + [ + "course_data_researcher", + "courses.legacy_data_researcher_permissions" + ], + [ + "course_editor", + "courses.create_files" + ], + [ + "course_editor", + "courses.edit_course_content" + ], + [ + "course_editor", + "courses.edit_details" + ], + [ + "course_editor", + "courses.edit_files" + ], + [ + "course_editor", + "courses.edit_grading_settings" + ], + [ + "course_editor", + "courses.manage_course_updates" + ], + [ + "course_editor", + "courses.manage_group_configurations" + ], + [ + "course_editor", + "courses.manage_library_updates" + ], + [ + "course_editor", + "courses.manage_pages_and_resources" + ], + [ + "course_editor", + "courses.manage_tags" + ], + [ + "course_editor", + "courses.view_advanced_settings" + ], + [ + "course_editor", + "courses.view_certificates" + ], + [ + "course_editor", + "courses.view_checklists" + ], + [ + "course_editor", + "courses.view_course" + ], + [ + "course_editor", + "courses.view_course_team" + ], + [ + "course_editor", + "courses.view_course_updates" + ], + [ + "course_editor", + "courses.view_files" + ], + [ + "course_editor", + "courses.view_grading_settings" + ], + [ + "course_editor", + "courses.view_group_configurations" + ], + [ + "course_editor", + "courses.view_library_updates" + ], + [ + "course_editor", + "courses.view_pages_and_resources" + ], + [ + "course_editor", + "courses.view_schedule_and_details" + ], + [ + "course_limited_staff", + "courses.legacy_limited_staff_role_permissions" + ], + [ + "course_staff", + "courses.create_files" + ], + [ + "course_staff", + "courses.delete_files" + ], + [ + "course_staff", + "courses.edit_course_content" + ], + [ + "course_staff", + "courses.edit_details" + ], + [ + "course_staff", + "courses.edit_files" + ], + [ + "course_staff", + "courses.edit_grading_settings" + ], + [ + "course_staff", + "courses.edit_schedule" + ], + [ + "course_staff", + "courses.export_course" + ], + [ + "course_staff", + "courses.export_tags" + ], + [ + "course_staff", + "courses.import_course" + ], + [ + "course_staff", + "courses.legacy_staff_role_permissions" + ], + [ + "course_staff", + "courses.manage_advanced_settings" + ], + [ + "course_staff", + "courses.manage_certificates" + ], + [ + "course_staff", + "courses.manage_course_updates" + ], + [ + "course_staff", + "courses.manage_group_configurations" + ], + [ + "course_staff", + "courses.manage_library_updates" + ], + [ + "course_staff", + "courses.manage_pages_and_resources" + ], + [ + "course_staff", + "courses.manage_tags" + ], + [ + "course_staff", + "courses.publish_course_content" + ], + [ + "course_staff", + "courses.view_advanced_settings" + ], + [ + "course_staff", + "courses.view_certificates" + ], + [ + "course_staff", + "courses.view_checklists" + ], + [ + "course_staff", + "courses.view_course" + ], + [ + "course_staff", + "courses.view_course_team" + ], + [ + "course_staff", + "courses.view_course_updates" + ], + [ + "course_staff", + "courses.view_files" + ], + [ + "course_staff", + "courses.view_grading_settings" + ], + [ + "course_staff", + "courses.view_group_configurations" + ], + [ + "course_staff", + "courses.view_library_updates" + ], + [ + "course_staff", + "courses.view_pages_and_resources" + ], + [ + "course_staff", + "courses.view_schedule_and_details" + ], + [ + "library_admin", + "content_libraries.create_library_collection" + ], + [ + "library_admin", + "content_libraries.delete_library" + ], + [ + "library_admin", + "content_libraries.delete_library_collection" + ], + [ + "library_admin", + "content_libraries.edit_library_collection" + ], + [ + "library_admin", + "content_libraries.edit_library_content" + ], + [ + "library_admin", + "content_libraries.manage_library_tags" + ], + [ + "library_admin", + "content_libraries.manage_library_team" + ], + [ + "library_admin", + "content_libraries.publish_library_content" + ], + [ + "library_admin", + "content_libraries.reuse_library_content" + ], + [ + "library_admin", + "content_libraries.view_library" + ], + [ + "library_admin", + "content_libraries.view_library_team" + ], + [ + "library_author", + "content_libraries.create_library_collection" + ], + [ + "library_author", + "content_libraries.delete_library_collection" + ], + [ + "library_author", + "content_libraries.edit_library_collection" + ], + [ + "library_author", + "content_libraries.edit_library_content" + ], + [ + "library_author", + "content_libraries.manage_library_tags" + ], + [ + "library_author", + "content_libraries.publish_library_content" + ], + [ + "library_author", + "content_libraries.reuse_library_content" + ], + [ + "library_author", + "content_libraries.view_library" + ], + [ + "library_author", + "content_libraries.view_library_team" + ], + [ + "library_contributor", + "content_libraries.create_library_collection" + ], + [ + "library_contributor", + "content_libraries.delete_library_collection" + ], + [ + "library_contributor", + "content_libraries.edit_library_collection" + ], + [ + "library_contributor", + "content_libraries.edit_library_content" + ], + [ + "library_contributor", + "content_libraries.manage_library_tags" + ], + [ + "library_contributor", + "content_libraries.reuse_library_content" + ], + [ + "library_contributor", + "content_libraries.view_library" + ], + [ + "library_contributor", + "content_libraries.view_library_team" + ], + [ + "library_user", + "content_libraries.reuse_library_content" + ], + [ + "library_user", + "content_libraries.view_library" + ], + [ + "library_user", + "content_libraries.view_library_team" + ] + ], + "roles": { + "course_admin": { + "description": "Can manage the course team and all course settings.", + "display_name": "Course Admin", + "hidden": false, + "icon": null, + "id": "course_admin", + "permissions": [ + "courses.create_files", + "courses.delete_files", + "courses.edit_course_content", + "courses.edit_details", + "courses.edit_files", + "courses.edit_grading_settings", + "courses.edit_schedule", + "courses.export_course", + "courses.export_tags", + "courses.import_course", + "courses.legacy_instructor_role_permissions", + "courses.manage_advanced_settings", + "courses.manage_certificates", + "courses.manage_course_team", + "courses.manage_course_updates", + "courses.manage_group_configurations", + "courses.manage_library_updates", + "courses.manage_pages_and_resources", + "courses.manage_tags", + "courses.manage_taxonomies", + "courses.publish_course_content", + "courses.view_advanced_settings", + "courses.view_certificates", + "courses.view_checklists", + "courses.view_course", + "courses.view_course_team", + "courses.view_course_updates", + "courses.view_files", + "courses.view_grading_settings", + "courses.view_group_configurations", + "courses.view_library_updates", + "courses.view_pages_and_resources", + "courses.view_schedule_and_details" + ], + "scopes": [ + "course-v1" + ] + }, + "course_auditor": { + "description": "Can view course content and settings, but cannot make changes.", + "display_name": "Course Auditor", + "hidden": true, + "icon": null, + "id": "course_auditor", + "permissions": [ + "courses.view_advanced_settings", + "courses.view_certificates", + "courses.view_checklists", + "courses.view_course", + "courses.view_course_team", + "courses.view_course_updates", + "courses.view_files", + "courses.view_grading_settings", + "courses.view_group_configurations", + "courses.view_library_updates", + "courses.view_pages_and_resources", + "courses.view_schedule_and_details" + ], + "scopes": [ + "course-v1" + ] + }, + "course_beta_tester": { + "description": "Legacy course role retained for backward compatibility. Grants the legacy beta tester compatibility permission.", + "display_name": "Course Beta Tester (legacy)", + "hidden": true, + "icon": null, + "id": "course_beta_tester", + "permissions": [ + "courses.legacy_beta_tester_permissions" + ], + "scopes": [ + "course-v1" + ] + }, + "course_data_researcher": { + "description": "Legacy course role retained for backward compatibility. Grants the legacy data researcher compatibility permission.", + "display_name": "Course Data Researcher (legacy)", + "hidden": true, + "icon": null, + "id": "course_data_researcher", + "permissions": [ + "courses.legacy_data_researcher_permissions" + ], + "scopes": [ + "course-v1" + ] + }, + "course_editor": { + "description": "Can create and edit course content, but cannot publish or change critical course settings.", + "display_name": "Course Editor", + "hidden": true, + "icon": null, + "id": "course_editor", + "permissions": [ + "courses.create_files", + "courses.edit_course_content", + "courses.edit_details", + "courses.edit_files", + "courses.edit_grading_settings", + "courses.manage_course_updates", + "courses.manage_group_configurations", + "courses.manage_library_updates", + "courses.manage_pages_and_resources", + "courses.manage_tags", + "courses.view_advanced_settings", + "courses.view_certificates", + "courses.view_checklists", + "courses.view_course", + "courses.view_course_team", + "courses.view_course_updates", + "courses.view_files", + "courses.view_grading_settings", + "courses.view_group_configurations", + "courses.view_library_updates", + "courses.view_pages_and_resources", + "courses.view_schedule_and_details" + ], + "scopes": [ + "course-v1" + ] + }, + "course_limited_staff": { + "description": "Legacy course role retained for backward compatibility. Grants the legacy limited staff compatibility permission.", + "display_name": "Course Limited Staff (legacy)", + "hidden": true, + "icon": null, + "id": "course_limited_staff", + "permissions": [ + "courses.legacy_limited_staff_role_permissions" + ], + "scopes": [ + "course-v1" + ] + }, + "course_staff": { + "description": "Can publish content and manage the course lifecycle in Studio.", + "display_name": "Course Staff", + "hidden": false, + "icon": null, + "id": "course_staff", + "permissions": [ + "courses.create_files", + "courses.delete_files", + "courses.edit_course_content", + "courses.edit_details", + "courses.edit_files", + "courses.edit_grading_settings", + "courses.edit_schedule", + "courses.export_course", + "courses.export_tags", + "courses.import_course", + "courses.legacy_staff_role_permissions", + "courses.manage_advanced_settings", + "courses.manage_certificates", + "courses.manage_course_updates", + "courses.manage_group_configurations", + "courses.manage_library_updates", + "courses.manage_pages_and_resources", + "courses.manage_tags", + "courses.publish_course_content", + "courses.view_advanced_settings", + "courses.view_certificates", + "courses.view_checklists", + "courses.view_course", + "courses.view_course_team", + "courses.view_course_updates", + "courses.view_files", + "courses.view_grading_settings", + "courses.view_group_configurations", + "courses.view_library_updates", + "courses.view_pages_and_resources", + "courses.view_schedule_and_details" + ], + "scopes": [ + "course-v1" + ] + }, + "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.", + "display_name": "Library Admin", + "hidden": false, + "icon": null, + "id": "library_admin", + "permissions": [ + "content_libraries.create_library_collection", + "content_libraries.delete_library", + "content_libraries.delete_library_collection", + "content_libraries.edit_library_collection", + "content_libraries.edit_library_content", + "content_libraries.manage_library_tags", + "content_libraries.manage_library_team", + "content_libraries.publish_library_content", + "content_libraries.reuse_library_content", + "content_libraries.view_library", + "content_libraries.view_library_team" + ], + "scopes": [ + "lib" + ] + }, + "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.", + "display_name": "Library Author", + "hidden": false, + "icon": null, + "id": "library_author", + "permissions": [ + "content_libraries.create_library_collection", + "content_libraries.delete_library_collection", + "content_libraries.edit_library_collection", + "content_libraries.edit_library_content", + "content_libraries.manage_library_tags", + "content_libraries.publish_library_content", + "content_libraries.reuse_library_content", + "content_libraries.view_library", + "content_libraries.view_library_team" + ], + "scopes": [ + "lib" + ] + }, + "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.", + "display_name": "Library Contributor", + "hidden": false, + "icon": null, + "id": "library_contributor", + "permissions": [ + "content_libraries.create_library_collection", + "content_libraries.delete_library_collection", + "content_libraries.edit_library_collection", + "content_libraries.edit_library_content", + "content_libraries.manage_library_tags", + "content_libraries.reuse_library_content", + "content_libraries.view_library", + "content_libraries.view_library_team" + ], + "scopes": [ + "lib" + ] + }, + "library_user": { + "description": "The Library User can view and reuse content but cannot edit or delete any resource.", + "display_name": "Library User", + "hidden": false, + "icon": null, + "id": "library_user", + "permissions": [ + "content_libraries.reuse_library_content", + "content_libraries.view_library", + "content_libraries.view_library_team" + ], + "scopes": [ + "lib" + ] + } + } +} diff --git a/src/openedx_authz/tests/applets/test_builtin_definitions.py b/src/openedx_authz/tests/applets/test_builtin_definitions.py new file mode 100644 index 00000000..49a64ac2 --- /dev/null +++ b/src/openedx_authz/tests/applets/test_builtin_definitions.py @@ -0,0 +1,77 @@ +"""Compatibility tests for definitions contributed by built-in applets.""" + +import json +from dataclasses import asdict +from pathlib import Path + +from openedx_authz.constants import roles +from openedx_authz.engine.schema.pipeline import SchemaPipeline + +COMPILED_SCHEMA_FIXTURE = Path(__file__).with_name("fixtures") / "compiled_schema.json" + + +def _schema_data(compiled): + """Return the behavior-bearing compiled schema fields in a stable form.""" + role_definitions = {} + for key, value in sorted(compiled.roles.items()): + definition = asdict(value.definition) + definition["permissions"] = sorted(definition["permissions"]) + role_definitions[key] = definition + + data = { + "categories": {key: asdict(value.definition) for key, value in sorted(compiled.categories.items())}, + "permissions": {key: asdict(value.definition) for key, value in sorted(compiled.permissions.items())}, + "roles": role_definitions, + "role_permission_pairs": compiled.role_permission_pairs(), + } + return json.loads(json.dumps(data)) + + +class TestBuiltinAppletDefinitions: + """Built-in applets compose the expected public authorization schema.""" + + def test_compiled_schema_matches_expected_definitions(self): + """The compiled applet schema matches the checked-in definition set.""" + compiled = SchemaPipeline().compile() + expected = json.loads(COMPILED_SCHEMA_FIXTURE.read_text(encoding="utf-8")) + + assert _schema_data(compiled) == expected + + def test_compiled_roles_match_public_role_constants(self): + """Every compiled role contains the permissions exposed by its public constant.""" + public_roles = ( + roles.LIBRARY_ADMIN, + roles.LIBRARY_AUTHOR, + roles.LIBRARY_CONTRIBUTOR, + roles.LIBRARY_USER, + roles.COURSE_ADMIN, + roles.COURSE_STAFF, + roles.COURSE_EDITOR, + roles.COURSE_AUDITOR, + roles.COURSE_LIMITED_STAFF, + roles.COURSE_DATA_RESEARCHER, + roles.COURSE_BETA_TESTER, + ) + + compiled = SchemaPipeline().compile() + + assert set(compiled.roles) == {role.external_key for role in public_roles} + for role in public_roles: + assert set(compiled.roles[role.external_key].definition.permissions) == set( + role.get_permission_identifiers() + ) + + def test_compiled_sources_belong_to_their_owning_applets(self): + """Compiled definitions expose their owning applet through source metadata.""" + compiled = SchemaPipeline().compile() + + assert compiled.roles["library_admin"].sources[0].module == ( + "openedx_authz.applets.content_libraries.schema" + ) + assert compiled.roles["course_admin"].sources[0].module == "openedx_authz.applets.course_team_roles.schema" + assert compiled.permissions["courses.view_course"].sources[0].module == ( + "openedx_authz.applets.course_authoring.schema" + ) + assert compiled.permissions["courses.view_course_team"].sources[0].module == ( + "openedx_authz.applets.course_authoring.schema" + ) diff --git a/src/openedx_authz/tests/authz/test_resources.py b/src/openedx_authz/tests/authz/test_resources.py index 59391b75..9894fbe4 100644 --- a/src/openedx_authz/tests/authz/test_resources.py +++ b/src/openedx_authz/tests/authz/test_resources.py @@ -12,6 +12,12 @@ from openedx_authz import authz +EXPECTED_SCHEMA_DIRECTORIES = ( + "openedx_authz/applets/content_libraries/schema", + "openedx_authz/applets/course_team_roles/schema", + "openedx_authz/applets/course_authoring/schema", +) + class TestSchemaProvider(TestCase): """Test the ``openedx_authz`` schema-provider entry point.""" @@ -20,6 +26,7 @@ def test_get_schema_resources_returns_declared_directories(self): """The entry point returns the declared directories as a fresh list.""" result = authz.get_schema_resources() + self.assertEqual(authz.SCHEMA_DIRECTORIES, EXPECTED_SCHEMA_DIRECTORIES) self.assertEqual(result, list(authz.SCHEMA_DIRECTORIES)) # Must be a list (a copy), not the underlying tuple, so callers can # mutate the result without affecting module state. diff --git a/src/openedx_authz/tests/schema/test_apply.py b/src/openedx_authz/tests/schema/test_apply.py index 423d9023..6efc2caf 100644 --- a/src/openedx_authz/tests/schema/test_apply.py +++ b/src/openedx_authz/tests/schema/test_apply.py @@ -454,7 +454,6 @@ def test_removed_role_and_category_are_pruned(self): assert AuthzPermissionDefinition.objects.count() == 0 assert AuthzPermissionCategory.objects.count() == 0 - @pytest.mark.django_db @pytest.mark.usefixtures("cache_invalidation") class TestDefinitionChangeReport: diff --git a/src/openedx_authz/tests/schema/test_discovery.py b/src/openedx_authz/tests/schema/test_discovery.py index 9cafdb60..8e22f6ab 100644 --- a/src/openedx_authz/tests/schema/test_discovery.py +++ b/src/openedx_authz/tests/schema/test_discovery.py @@ -21,12 +21,16 @@ SchemaDiscoveryError, ) -SCHEMA_DIR = "openedx_authz/authz/schema" -EXPECTED_FILES = { - "course_permissions.yaml", - "course_roles.yaml", - "library_permissions.yaml", - "library_roles.yaml", +SCHEMA_DIR = "openedx_authz/applets/content_libraries/schema" +EXPECTED_FILES = {"permissions.yaml", "roles.yaml"} +EXPECTED_BUILTIN_PATHS = { + "applets/content_libraries/schema/permissions.yaml", + "applets/content_libraries/schema/roles.yaml", + "applets/course_authoring/schema/permissions.yaml", + "applets/course_authoring/schema/role_extensions.yaml", + "applets/course_team_roles/schema/legacy_permissions.yaml", + "applets/course_team_roles/schema/role_extensions.yaml", + "applets/course_team_roles/schema/roles.yaml", } @@ -35,21 +39,27 @@ class TestDirectoryExpansion: def test_directory_is_expanded_to_yaml_files(self): """Passing a directory yields exactly its YAML files, by name.""" - resources = SchemaDiscovery(passed_in_directories=[SCHEMA_DIR]).discover() + resources = SchemaDiscovery()._iter_directory( # pylint: disable=protected-access + SCHEMA_DIR, origin=Origin.PASSED_IN + ) names = {r.resource_path.rsplit("/", 1)[-1] for r in resources} assert names == EXPECTED_FILES def test_discovered_resource_anchors_and_module_are_set(self): """Each resource records its import anchor, resource path, and module.""" - resources = SchemaDiscovery(passed_in_directories=[SCHEMA_DIR]).discover() + resources = SchemaDiscovery()._iter_directory( # pylint: disable=protected-access + SCHEMA_DIR, origin=Origin.PASSED_IN + ) sample = resources[0] assert sample.package == "openedx_authz" # importable anchor - assert sample.resource_path.startswith("authz/schema/") - assert sample.module == "openedx_authz.authz.schema" # source identity + assert sample.resource_path.startswith("applets/content_libraries/schema/") + assert sample.module == "openedx_authz.applets.content_libraries.schema" # source identity def test_contents_are_readable(self): """A discovered resource can be read back as non-empty bytes.""" - resources = SchemaDiscovery(passed_in_directories=[SCHEMA_DIR]).discover() + resources = SchemaDiscovery()._iter_directory( # pylint: disable=protected-access + SCHEMA_DIR, origin=Origin.PASSED_IN + ) assert resources[0].read_bytes() # non-empty bytes def test_discovery_is_deterministic(self): @@ -66,14 +76,18 @@ def test_unknown_directory_raises(self): def test_own_entry_point_directory_is_discovered(self): """The installed ``authz.schema`` entry point yields this package's files.""" resources = SchemaDiscovery().discover() - names = {r.resource_path.rsplit("/", 1)[-1] for r in resources} - assert EXPECTED_FILES.issubset(names) + paths = {resource.resource_path for resource in resources if resource.package == "openedx_authz"} + assert paths == EXPECTED_BUILTIN_PATHS def test_duplicate_directories_are_deduplicated(self): """The same directory passed twice yields each file only once.""" resources = SchemaDiscovery(passed_in_directories=[SCHEMA_DIR, SCHEMA_DIR]).discover() - paths = [r.resource_path for r in resources] - assert len(paths) == len(set(paths)) == len(EXPECTED_FILES) + content_library_paths = [ + resource.resource_path + for resource in resources + if resource.module == "openedx_authz.applets.content_libraries.schema" + ] + assert len(content_library_paths) == len(set(content_library_paths)) == len(EXPECTED_FILES) class TestSettingsDirectories: @@ -167,7 +181,7 @@ def test_entry_point_origin_wins_over_passed_in_duplicate(self): resources = SchemaDiscovery(passed_in_directories=[SCHEMA_DIR]).discover() assert {r.origin for r in resources} == {Origin.ENTRY_POINT} - assert len(resources) == len(EXPECTED_FILES) + assert {resource.resource_path for resource in resources} == EXPECTED_BUILTIN_PATHS def test_passed_in_only_directory_is_marked_passed_in(self): """A directory reached only via the constructor keeps the passed-in origin.""" diff --git a/src/openedx_authz/tests/schema/test_loading.py b/src/openedx_authz/tests/schema/test_loading.py index 8dc1bf7d..78db20d5 100644 --- a/src/openedx_authz/tests/schema/test_loading.py +++ b/src/openedx_authz/tests/schema/test_loading.py @@ -163,7 +163,7 @@ def _distribution(name): def test_installed_package_resolves_to_its_distribution(self): """A module shipped by this package resolves to the real distribution.""" - discovery = SchemaDiscovery(passed_in_directories=["openedx_authz/authz/schema"]) + discovery = SchemaDiscovery(passed_in_directories=["openedx_authz/applets/content_libraries/schema"]) docs = SchemaLoader().load(discovery.discover()) assert {doc.source.distribution for doc in docs} == {"openedx-authz"} @@ -171,10 +171,13 @@ def test_installed_package_resolves_to_its_distribution(self): def test_module_is_recorded_as_the_dotted_path(self): """The source module is recorded as the resource's dotted import path.""" - discovery = SchemaDiscovery(passed_in_directories=["openedx_authz/authz/schema"]) + discovery = SchemaDiscovery(passed_in_directories=["openedx_authz/applets/content_libraries/schema"]) docs = SchemaLoader().load(discovery.discover()) - assert {doc.source.module for doc in docs} == {"openedx_authz.authz.schema"} + content_library_docs = [ + doc for doc in docs if doc.source.module == "openedx_authz.applets.content_libraries.schema" + ] + assert len(content_library_docs) == 2 def test_unknown_package_falls_back_to_its_top_level_name(self): """An operator-supplied directory need not belong to a distribution.""" diff --git a/src/openedx_authz/tests/test_imports.py b/src/openedx_authz/tests/test_imports.py index 6efedb19..4d9f69eb 100644 --- a/src/openedx_authz/tests/test_imports.py +++ b/src/openedx_authz/tests/test_imports.py @@ -1,8 +1,11 @@ """Test module imports.""" import sys +from importlib import import_module from unittest import TestCase +from openedx_authz.constants import permissions, roles + class TestImports(TestCase): """Test that imports work correctly.""" @@ -35,3 +38,39 @@ def test_import_authzenforcer(self): self.assertIsNotNone(AuthzEnforcer) except ImportError as e: self.fail(f"Failed to import AuthzEnforcer: {e}") + + def test_applet_permission_constants_are_reexported(self): + """Existing permission imports refer to the objects owned by each applet.""" + applet_constants = { + "openedx_authz.applets.content_libraries.constants": ("VIEW_LIBRARY",), + "openedx_authz.applets.course_authoring.constants": ( + "COURSES_VIEW_COURSE", + "COURSES_VIEW_COURSE_TEAM", + "COURSES_MANAGE_COURSE_TEAM", + ), + "openedx_authz.applets.course_team_roles.constants": ( + "COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS", + ), + } + + for module_name, names in applet_constants.items(): + module = import_module(module_name) + for name in names: + with self.subTest(module=module_name, name=name): + self.assertIs(getattr(permissions, name), getattr(module, name)) + + def test_content_library_roles_are_reexported(self): + """Existing Library role imports refer to the applet-owned role objects.""" + applet_constants = import_module("openedx_authz.applets.content_libraries.roles") + + for name in ("LIBRARY_ADMIN", "LIBRARY_AUTHOR", "LIBRARY_CONTRIBUTOR", "LIBRARY_USER"): + with self.subTest(name=name): + self.assertIs(getattr(roles, name), getattr(applet_constants, name)) + + def test_course_team_roles_are_reexported(self): + """Existing course role imports refer to the applet-owned role objects.""" + applet_roles = import_module("openedx_authz.applets.course_team_roles.roles") + + for name in applet_roles.__all__: + with self.subTest(name=name): + self.assertIs(getattr(roles, name), getattr(applet_roles, name)) diff --git a/src/openedx_authz/tests/test_migrations.py b/src/openedx_authz/tests/test_migrations.py index 53b63a66..edf68ae1 100644 --- a/src/openedx_authz/tests/test_migrations.py +++ b/src/openedx_authz/tests/test_migrations.py @@ -1,8 +1,10 @@ """Unit Tests for openedx_authz migrations.""" +from importlib import import_module from unittest.mock import MagicMock, patch from ddt import data, ddt, unpack +from django.apps import apps from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.core.management import CommandError, call_command @@ -48,6 +50,17 @@ ScopeType, Status, ) +from openedx_authz.models.schema import ( + AuthzCategorySource, + AuthzPermissionCategory, + AuthzPermissionDefinition, + AuthzPermissionSource, + AuthzRoleDefinition, + AuthzRolePermission, + AuthzRolePermissionSource, + AuthzRoleSource, + AuthzSchemaSource, +) from openedx_authz.models.subjects import UserSubject from openedx_authz.tests.stubs.models import ( ContentLibrary, @@ -72,6 +85,89 @@ error_group_name = f"{OBJECT_PREFIX}error_group" empty_group_name = f"{OBJECT_PREFIX}empty_group" +schema_source_migration = import_module( + "openedx_authz.migrations.0012_move_builtin_schema_sources_to_applets" +) + + +class TestBuiltinSchemaSourceMigration(TestCase): + """Released shared-schema source links move to their owning applets.""" + + def test_source_links_are_distributed_by_definition_owner(self): + """Each source-link type moves from the shared module to its owner.""" + old_source = AuthzSchemaSource.objects.create( + distribution="openedx-authz", + distribution_version="1.34.0", + module=schema_source_migration.OLD_MODULE, + schema_version="1.0", + ) + category = AuthzPermissionCategory.objects.create(category_id="course_team_group", display_name="Course team") + permission = AuthzPermissionDefinition.objects.create( + namespace="courses", + name="view_course_team", + display_name="View course team", + category=category, + scopes=["course-v1"], + ) + role = AuthzRoleDefinition.objects.create( + role_id="course_admin", + display_name="Course Admin", + scopes=["course-v1"], + ) + role_permission = AuthzRolePermission.objects.create( + role=role, + permission=permission, + scope="course-v1", + ) + AuthzCategorySource.objects.create(category=category, source=old_source) + AuthzPermissionSource.objects.create(permission=permission, source=old_source) + AuthzRoleSource.objects.create(role=role, source=old_source) + AuthzRolePermissionSource.objects.create(role_permission=role_permission, source=old_source) + + schema_source_migration.move_builtin_schema_sources(apps, None) + + self.assertFalse(AuthzSchemaSource.objects.filter(module=schema_source_migration.OLD_MODULE).exists()) + course_authoring = schema_source_migration.APPLET_MODULES["course_authoring"] + course_team_roles = schema_source_migration.APPLET_MODULES["course_team_roles"] + self.assertListEqual( + list(category.sources.values_list("module", flat=True)), + [course_authoring], + ) + self.assertListEqual(list(permission.sources.values_list("module", flat=True)), [course_authoring]) + self.assertListEqual(list(role.sources.values_list("module", flat=True)), [course_team_roles]) + self.assertListEqual(list(role_permission.sources.values_list("module", flat=True)), [course_authoring]) + + def test_existing_applet_link_is_preserved(self): + """An existing destination link keeps its metadata when the old link is removed.""" + old_source = AuthzSchemaSource.objects.create( + distribution="openedx-authz", + module=schema_source_migration.OLD_MODULE, + ) + applet_source = AuthzSchemaSource.objects.create( + distribution="openedx-authz", + module=schema_source_migration.APPLET_MODULES["course_authoring"], + ) + category = AuthzPermissionCategory.objects.create(category_id="course_files", display_name="Files") + AuthzCategorySource.objects.create(category=category, source=old_source, priority=10) + AuthzCategorySource.objects.create(category=category, source=applet_source, priority=20) + + schema_source_migration.move_builtin_schema_sources(apps, None) + + link = AuthzCategorySource.objects.get(category=category) + self.assertEqual(link.source, applet_source) + self.assertEqual(link.priority, 20) + + def test_missing_shared_source_is_a_noop(self): + """The migration does not load link models when the shared source is absent.""" + migration_apps = MagicMock() + Source = migration_apps.get_model.return_value + Source.objects.filter.return_value.exists.return_value = False + + schema_source_migration.move_builtin_schema_sources(migration_apps, None) + + migration_apps.get_model.assert_called_once_with("openedx_authz", "AuthzSchemaSource") + Source.objects.filter.assert_called_once_with(module=schema_source_migration.OLD_MODULE) + class TestLegacyContentLibraryPermissionsMigration(TestCase): """Test cases for migrating legacy content library permissions.""" From 062e5e9710da43cee88a5ac4994ba6b149f9520c Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 20:30:18 +0200 Subject: [PATCH 02/11] docs: separate YAML definitions from Python constants in ADR 0032 Co-Authored-By: Claude Opus 5.5 --- ...or-built-in-authorization-capabilities.rst | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/decisions/0032-applets-for-built-in-authorization-capabilities.rst b/docs/decisions/0032-applets-for-built-in-authorization-capabilities.rst index 579d5b3c..7449ac87 100644 --- a/docs/decisions/0032-applets-for-built-in-authorization-capabilities.rst +++ b/docs/decisions/0032-applets-for-built-in-authorization-capabilities.rst @@ -124,14 +124,21 @@ Roles such as ``course_editor`` have no permissions of their own, so schema vers REST endpoints continue to follow the ownership rules in `ADR 0026`_, and applets now give those rules a package boundary. The Admin Console endpoints therefore move to ``role_management``, since they support the same role-management workflow, although this placement does not settle the ownership of the organization data returned by ``AdminConsoleOrgsAPIView``, and ``WaffleFlagStatesAPIView`` moves to ``course_authoring``. In both cases, the existing URLs and route names remain unchanged. -5. Imports -========== +5. YAML definitions and Python constants +======================================== + +YAML is the source of truth for static definitions. It defines every permission, role, and role extension together with their metadata, and it is the only place that lists a role's permissions, so the schema pipeline composes each role's complete permission list from the ``role_extensions`` contributed by the applets. + +Python constants give clients stable identifiers to reference in code, since permission constants name the actions that clients check through the public API and role constants name the roles that clients assign. Role constants do not list permissions, because listing them in Python would repeat the schema composition and make each capability applet import and extend roles owned by ``course_team_roles``, so code that needs a role's permissions reads them through the public API or the compiled schema. -Although applets are internal packages, ``openedx_authz.constants.permissions`` and ``openedx_authz.constants.roles`` continue to export every existing constant, while ``COURSE_ADMIN`` and ``COURSE_STAFF`` continue to contain their complete permission lists, so existing import paths remain valid when code moves into an applet. +Every permission constant names a permission defined in YAML, and every permission defined in YAML has a constant. Each applet maintains its permission constants manually for now, tests compare them with the compiled schema, and generating them from YAML may replace this process in later work. + +6. Imports +========== -YAML remains the source of truth for static definitions, although each applet will maintain its Python constants manually for now, so tests compare those constants with the applet's YAML definitions, and generating them from YAML may replace this process in later work. +Applets are internal packages, so clients import the permission and role constants from ``openedx_authz.constants.permissions`` and ``openedx_authz.constants.roles``, which re-export each applet's constants and keep existing import paths valid when code moves into an applet. -6. Future extraction +7. Future extraction ==================== The applets remain inside ``openedx-authz`` for this work, so implementing the ownership model and the permissions from `Issue #484`_ does not require coordinated changes across the services that use the library, and since their definitions describe the underlying actions and data rather than one user interface, another client can reuse the same permissions when it exposes the same responsibility. @@ -143,11 +150,12 @@ Consequences 1. Each built-in definition has an explicit owner based on the capability it protects. 2. Adding a capability does not require adding its definitions to shared course permission and role files. -3. The complete permission list for a course role is assembled from several schema contributions, so readers use the compiled schema or the public role constants to inspect that list. +3. The complete permission list for a course role is assembled from several schema contributions, so readers use the compiled schema or the public API to inspect that list. 4. Each permission identifier appears in YAML and Python until generated constants replace the manually maintained files. -5. The Admin Console and course-authoring endpoints move again after `ADR 0026`_, although their public URLs, route names, request and response contracts, and import paths remain stable. -6. Reviewers must identify unwanted dependencies between applets because this decision does not add automated import-boundary checks. -7. Applets create boundaries that can support later extraction, although moving one to another service requires a separate decision about dependencies and compatibility. +5. Role constants no longer list permissions, so code that read a role's permissions from its constant uses the public API instead. +6. The Admin Console and course-authoring endpoints move again after `ADR 0026`_, although their public URLs, route names, request and response contracts, and import paths remain stable. +7. Reviewers must identify unwanted dependencies between applets because this decision does not add automated import-boundary checks. +8. Applets create boundaries that can support later extraction, although moving one to another service requires a separate decision about dependencies and compatibility. Rejected Alternatives ********************* From 6a2c1e8b77ce42d23e106c5933564e0f2e01f5a9 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 20:30:22 +0200 Subject: [PATCH 03/11] refactor: keep role permissions only in the YAML schema Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.rst | 8 +- .../applets/content_libraries/README.rst | 9 +- .../applets/content_libraries/roles.py | 58 +----- .../applets/course_team_roles/README.rst | 10 +- .../applets/course_team_roles/roles.py | 177 ++---------------- src/openedx_authz/tests/api/test_roles.py | 53 ++---- src/openedx_authz/tests/api/test_users.py | 16 +- .../tests/applets/test_builtin_definitions.py | 6 +- .../tests/rest_api/test_views.py | 3 +- src/openedx_authz/tests/role_permissions.py | 61 ++++++ 10 files changed, 109 insertions(+), 292 deletions(-) create mode 100644 src/openedx_authz/tests/role_permissions.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3e19dab0..447f57b9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -56,8 +56,12 @@ Changed ======== * Static roles may declare an empty permission list when ``role_extensions`` contribute their permissions. Permission lists used by extension edits remain non-empty. -* Built-in Content Library and course authorization definitions now live in capability applets, - while the existing public imports and compiled authorization schema remain unchanged. +* Built-in Content Library and course authorization definitions now live in capability applets. The compiled authorization schema, the permission constants, and the role keys remain unchanged. + +Removed +======= + +* Removed the role permission lists from ``openedx_authz.constants.roles``, such as ``COURSE_ADMIN_PERMISSIONS`` and ``LIBRARY_ADMIN_PERMISSIONS``. Role constants now identify a role and have an empty ``permissions`` list. Use ``openedx_authz.api.get_permissions_for_single_role`` or the compiled schema to read a role's permissions. Fixed ===== diff --git a/src/openedx_authz/applets/content_libraries/README.rst b/src/openedx_authz/applets/content_libraries/README.rst index 4df6c4ea..43fde115 100644 --- a/src/openedx_authz/applets/content_libraries/README.rst +++ b/src/openedx_authz/applets/content_libraries/README.rst @@ -1,11 +1,6 @@ Content Libraries ################# -This applet owns the built-in roles and permissions for Content Libraries. Its -``schema`` directory is loaded through the package's existing ``authz.schema`` -entry point, while ``constants.py`` contains its permission objects and -``roles.py`` contains its role objects. +This applet owns the built-in roles and permissions for Content Libraries. Its ``schema`` directory is loaded through the package's existing ``authz.schema`` entry point, while ``constants.py`` contains its permission objects and ``roles.py`` contains its role constants, which identify each role without listing its permissions. -The applet is an internal package. Existing consumers should continue importing -Library constants from ``openedx_authz.constants.permissions`` and -``openedx_authz.constants.roles``, which re-export the objects defined here. +The applet is an internal package, so clients import Library constants from ``openedx_authz.constants.permissions`` and ``openedx_authz.constants.roles``, which re-export the objects defined here. diff --git a/src/openedx_authz/applets/content_libraries/roles.py b/src/openedx_authz/applets/content_libraries/roles.py index 3e3e3b28..3852c1cc 100644 --- a/src/openedx_authz/applets/content_libraries/roles.py +++ b/src/openedx_authz/applets/content_libraries/roles.py @@ -1,60 +1,18 @@ -"""Content Library role constants.""" +"""Content Library role constants. -from openedx_authz.api.data import RoleData -from openedx_authz.applets.content_libraries import constants as permissions +Each constant identifies a role. Its permissions are defined in ``schema/roles.yaml``. +""" -LIBRARY_ADMIN_PERMISSIONS = [ - permissions.VIEW_LIBRARY, - permissions.MANAGE_LIBRARY_TAGS, - permissions.DELETE_LIBRARY, - permissions.EDIT_LIBRARY_CONTENT, - permissions.PUBLISH_LIBRARY_CONTENT, - permissions.REUSE_LIBRARY_CONTENT, - permissions.VIEW_LIBRARY_TEAM, - permissions.MANAGE_LIBRARY_TEAM, - permissions.CREATE_LIBRARY_COLLECTION, - permissions.EDIT_LIBRARY_COLLECTION, - permissions.DELETE_LIBRARY_COLLECTION, -] -LIBRARY_AUTHOR_PERMISSIONS = [ - permissions.VIEW_LIBRARY, - permissions.MANAGE_LIBRARY_TAGS, - permissions.EDIT_LIBRARY_CONTENT, - permissions.PUBLISH_LIBRARY_CONTENT, - permissions.REUSE_LIBRARY_CONTENT, - permissions.VIEW_LIBRARY_TEAM, - permissions.CREATE_LIBRARY_COLLECTION, - permissions.EDIT_LIBRARY_COLLECTION, - permissions.DELETE_LIBRARY_COLLECTION, -] -LIBRARY_CONTRIBUTOR_PERMISSIONS = [ - permissions.VIEW_LIBRARY, - permissions.MANAGE_LIBRARY_TAGS, - permissions.EDIT_LIBRARY_CONTENT, - permissions.REUSE_LIBRARY_CONTENT, - permissions.VIEW_LIBRARY_TEAM, - permissions.CREATE_LIBRARY_COLLECTION, - permissions.EDIT_LIBRARY_COLLECTION, - permissions.DELETE_LIBRARY_COLLECTION, -] -LIBRARY_USER_PERMISSIONS = [ - permissions.VIEW_LIBRARY, - permissions.REUSE_LIBRARY_CONTENT, - permissions.VIEW_LIBRARY_TEAM, -] +from openedx_authz.api.data import RoleData -LIBRARY_ADMIN = RoleData(external_key="library_admin", permissions=LIBRARY_ADMIN_PERMISSIONS) -LIBRARY_AUTHOR = RoleData(external_key="library_author", permissions=LIBRARY_AUTHOR_PERMISSIONS) -LIBRARY_CONTRIBUTOR = RoleData(external_key="library_contributor", permissions=LIBRARY_CONTRIBUTOR_PERMISSIONS) -LIBRARY_USER = RoleData(external_key="library_user", permissions=LIBRARY_USER_PERMISSIONS) +LIBRARY_ADMIN = RoleData(external_key="library_admin") +LIBRARY_AUTHOR = RoleData(external_key="library_author") +LIBRARY_CONTRIBUTOR = RoleData(external_key="library_contributor") +LIBRARY_USER = RoleData(external_key="library_user") __all__ = [ "LIBRARY_ADMIN", - "LIBRARY_ADMIN_PERMISSIONS", "LIBRARY_AUTHOR", - "LIBRARY_AUTHOR_PERMISSIONS", "LIBRARY_CONTRIBUTOR", - "LIBRARY_CONTRIBUTOR_PERMISSIONS", "LIBRARY_USER", - "LIBRARY_USER_PERMISSIONS", ] diff --git a/src/openedx_authz/applets/course_team_roles/README.rst b/src/openedx_authz/applets/course_team_roles/README.rst index 7c3afb4b..c2e45910 100644 --- a/src/openedx_authz/applets/course_team_roles/README.rst +++ b/src/openedx_authz/applets/course_team_roles/README.rst @@ -1,12 +1,6 @@ Course-Team Roles ################# -This applet owns the identifiers and metadata for roles held by members of a -course team, including the legacy compatibility roles. Their base definitions -have empty permission lists because the applets that own each permission add it -through role extensions. +This applet owns the identifiers and metadata for roles held by members of a course team, including the legacy compatibility roles. Their base definitions have empty permission lists because the applets that own each permission add it through role extensions. -The legacy compatibility permissions also live here because they connect these -roles to the previous course-role system. ``roles.py`` composes the complete -role constants from each applet's permission contributions, while existing -consumers can continue importing them from ``openedx_authz.constants``. +The legacy compatibility permissions also live here because they connect these roles to the previous course-role system. ``roles.py`` contains the role constants, which identify each role without listing its permissions. Existing consumers continue importing them from ``openedx_authz.constants``. diff --git a/src/openedx_authz/applets/course_team_roles/roles.py b/src/openedx_authz/applets/course_team_roles/roles.py index 6d0bbfe6..aaf3363d 100644 --- a/src/openedx_authz/applets/course_team_roles/roles.py +++ b/src/openedx_authz/applets/course_team_roles/roles.py @@ -1,9 +1,10 @@ """ -Default roles and their associated permissions. +Course-team role constants. + +Each constant identifies a role. Its permissions are defined in the applets' YAML schema files. """ from openedx_authz.api.data import RoleData -from openedx_authz.applets.course_authoring import constants as course_authoring from openedx_authz.applets.course_team_roles.constants import ( COURSE_ADMIN_ID, COURSE_AUDITOR_ID, @@ -12,168 +13,20 @@ COURSE_EDITOR_ID, COURSE_LIMITED_STAFF_ID, COURSE_STAFF_ID, - COURSES_LEGACY_BETA_TESTER_PERMISSIONS, - COURSES_LEGACY_DATA_RESEARCHER_PERMISSIONS, - COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS, - COURSES_LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS, - COURSES_LEGACY_STAFF_ROLE_PERMISSIONS, -) - -# Course Roles and Permissions - -COURSE_AUDITOR_PERMISSIONS = [ - course_authoring.COURSES_VIEW_COURSE, - course_authoring.COURSES_VIEW_COURSE_UPDATES, - course_authoring.COURSES_VIEW_PAGES_AND_RESOURCES, - course_authoring.COURSES_VIEW_FILES, - course_authoring.COURSES_VIEW_GRADING_SETTINGS, - course_authoring.COURSES_VIEW_CHECKLISTS, - course_authoring.COURSES_VIEW_COURSE_TEAM, - course_authoring.COURSES_VIEW_SCHEDULE_AND_DETAILS, - course_authoring.COURSES_VIEW_ADVANCED_SETTINGS, - course_authoring.COURSES_VIEW_CERTIFICATES, - course_authoring.COURSES_VIEW_GROUP_CONFIGURATIONS, - course_authoring.COURSES_VIEW_LIBRARY_UPDATES, -] - -COURSE_AUDITOR = RoleData( - external_key=COURSE_AUDITOR_ID, - permissions=COURSE_AUDITOR_PERMISSIONS, -) - -COURSE_EDITOR_PERMISSIONS = [ - course_authoring.COURSES_VIEW_COURSE, - course_authoring.COURSES_VIEW_COURSE_UPDATES, - course_authoring.COURSES_VIEW_PAGES_AND_RESOURCES, - course_authoring.COURSES_VIEW_FILES, - course_authoring.COURSES_VIEW_GRADING_SETTINGS, - course_authoring.COURSES_VIEW_CHECKLISTS, - course_authoring.COURSES_VIEW_COURSE_TEAM, - course_authoring.COURSES_VIEW_SCHEDULE_AND_DETAILS, - course_authoring.COURSES_VIEW_ADVANCED_SETTINGS, - course_authoring.COURSES_VIEW_CERTIFICATES, - course_authoring.COURSES_VIEW_GROUP_CONFIGURATIONS, - course_authoring.COURSES_VIEW_LIBRARY_UPDATES, - course_authoring.COURSES_EDIT_COURSE_CONTENT, - course_authoring.COURSES_MANAGE_LIBRARY_UPDATES, - course_authoring.COURSES_MANAGE_COURSE_UPDATES, - course_authoring.COURSES_MANAGE_PAGES_AND_RESOURCES, - course_authoring.COURSES_CREATE_FILES, - course_authoring.COURSES_EDIT_FILES, - course_authoring.COURSES_EDIT_GRADING_SETTINGS, - course_authoring.COURSES_MANAGE_GROUP_CONFIGURATIONS, - course_authoring.COURSES_EDIT_DETAILS, - course_authoring.COURSES_MANAGE_TAGS, -] - -COURSE_EDITOR = RoleData(external_key=COURSE_EDITOR_ID, permissions=COURSE_EDITOR_PERMISSIONS) - -COURSE_ADMIN_PERMISSIONS = [ - COURSES_LEGACY_INSTRUCTOR_ROLE_PERMISSIONS, - course_authoring.COURSES_VIEW_COURSE, - course_authoring.COURSES_VIEW_COURSE_UPDATES, - course_authoring.COURSES_VIEW_PAGES_AND_RESOURCES, - course_authoring.COURSES_VIEW_FILES, - course_authoring.COURSES_VIEW_GRADING_SETTINGS, - course_authoring.COURSES_VIEW_CHECKLISTS, - course_authoring.COURSES_VIEW_COURSE_TEAM, - course_authoring.COURSES_VIEW_SCHEDULE_AND_DETAILS, - course_authoring.COURSES_VIEW_ADVANCED_SETTINGS, - course_authoring.COURSES_VIEW_CERTIFICATES, - course_authoring.COURSES_VIEW_GROUP_CONFIGURATIONS, - course_authoring.COURSES_VIEW_LIBRARY_UPDATES, - course_authoring.COURSES_EDIT_COURSE_CONTENT, - course_authoring.COURSES_MANAGE_LIBRARY_UPDATES, - course_authoring.COURSES_MANAGE_COURSE_UPDATES, - course_authoring.COURSES_MANAGE_PAGES_AND_RESOURCES, - course_authoring.COURSES_CREATE_FILES, - course_authoring.COURSES_EDIT_FILES, - course_authoring.COURSES_EDIT_GRADING_SETTINGS, - course_authoring.COURSES_MANAGE_GROUP_CONFIGURATIONS, - course_authoring.COURSES_EDIT_DETAILS, - course_authoring.COURSES_MANAGE_TAGS, - course_authoring.COURSES_PUBLISH_COURSE_CONTENT, - course_authoring.COURSES_DELETE_FILES, - course_authoring.COURSES_EDIT_SCHEDULE, - course_authoring.COURSES_MANAGE_ADVANCED_SETTINGS, - course_authoring.COURSES_MANAGE_CERTIFICATES, - course_authoring.COURSES_IMPORT_COURSE, - course_authoring.COURSES_EXPORT_COURSE, - course_authoring.COURSES_EXPORT_TAGS, - course_authoring.COURSES_MANAGE_COURSE_TEAM, - course_authoring.COURSES_MANAGE_TAXONOMIES, -] - -COURSE_ADMIN = RoleData(external_key=COURSE_ADMIN_ID, permissions=COURSE_ADMIN_PERMISSIONS) - -COURSE_STAFF_PERMISSIONS = [ - COURSES_LEGACY_STAFF_ROLE_PERMISSIONS, - course_authoring.COURSES_VIEW_COURSE, - course_authoring.COURSES_VIEW_COURSE_UPDATES, - course_authoring.COURSES_VIEW_PAGES_AND_RESOURCES, - course_authoring.COURSES_VIEW_FILES, - course_authoring.COURSES_VIEW_GRADING_SETTINGS, - course_authoring.COURSES_VIEW_CHECKLISTS, - course_authoring.COURSES_VIEW_COURSE_TEAM, - course_authoring.COURSES_VIEW_SCHEDULE_AND_DETAILS, - course_authoring.COURSES_VIEW_ADVANCED_SETTINGS, - course_authoring.COURSES_VIEW_CERTIFICATES, - course_authoring.COURSES_VIEW_GROUP_CONFIGURATIONS, - course_authoring.COURSES_VIEW_LIBRARY_UPDATES, - course_authoring.COURSES_EDIT_COURSE_CONTENT, - course_authoring.COURSES_MANAGE_LIBRARY_UPDATES, - course_authoring.COURSES_MANAGE_COURSE_UPDATES, - course_authoring.COURSES_MANAGE_PAGES_AND_RESOURCES, - course_authoring.COURSES_CREATE_FILES, - course_authoring.COURSES_EDIT_FILES, - course_authoring.COURSES_EDIT_GRADING_SETTINGS, - course_authoring.COURSES_MANAGE_GROUP_CONFIGURATIONS, - course_authoring.COURSES_EDIT_DETAILS, - course_authoring.COURSES_MANAGE_TAGS, - course_authoring.COURSES_PUBLISH_COURSE_CONTENT, - course_authoring.COURSES_DELETE_FILES, - course_authoring.COURSES_EDIT_SCHEDULE, - course_authoring.COURSES_MANAGE_ADVANCED_SETTINGS, - course_authoring.COURSES_MANAGE_CERTIFICATES, - course_authoring.COURSES_IMPORT_COURSE, - course_authoring.COURSES_EXPORT_COURSE, - course_authoring.COURSES_EXPORT_TAGS, -] - -COURSE_STAFF = RoleData(external_key=COURSE_STAFF_ID, permissions=COURSE_STAFF_PERMISSIONS) - -COURSE_LIMITED_STAFF_PERMISSIONS = [ - COURSES_LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS, -] - -COURSE_LIMITED_STAFF = RoleData( - external_key=COURSE_LIMITED_STAFF_ID, - permissions=COURSE_LIMITED_STAFF_PERMISSIONS, ) -COURSE_DATA_RESEARCHER_PERMISSIONS = [ - COURSES_LEGACY_DATA_RESEARCHER_PERMISSIONS, -] - -COURSE_DATA_RESEARCHER = RoleData( - external_key=COURSE_DATA_RESEARCHER_ID, - permissions=COURSE_DATA_RESEARCHER_PERMISSIONS, -) - -COURSE_BETA_TESTER_PERMISSIONS = [ - COURSES_LEGACY_BETA_TESTER_PERMISSIONS, -] - -COURSE_BETA_TESTER = RoleData( - external_key=COURSE_BETA_TESTER_ID, - permissions=COURSE_BETA_TESTER_PERMISSIONS, -) +COURSE_AUDITOR = RoleData(external_key=COURSE_AUDITOR_ID) +COURSE_EDITOR = RoleData(external_key=COURSE_EDITOR_ID) +COURSE_ADMIN = RoleData(external_key=COURSE_ADMIN_ID) +COURSE_STAFF = RoleData(external_key=COURSE_STAFF_ID) +COURSE_LIMITED_STAFF = RoleData(external_key=COURSE_LIMITED_STAFF_ID) +COURSE_DATA_RESEARCHER = RoleData(external_key=COURSE_DATA_RESEARCHER_ID) +COURSE_BETA_TESTER = RoleData(external_key=COURSE_BETA_TESTER_ID) # This is a known LMS-only permission, but doesn't actually grant anything yet. # # It is intended to be handled in the Willow time frame. -CCX_COACH_PERMISSIONS = [] -CCX_COACH = RoleData(external_key="ccx_coach", permissions=CCX_COACH_PERMISSIONS) +CCX_COACH = RoleData(external_key="ccx_coach") # Map of legacy course role names to their equivalent new roles @@ -190,20 +43,12 @@ __all__ = [ "CCX_COACH", - "CCX_COACH_PERMISSIONS", "COURSE_ADMIN", - "COURSE_ADMIN_PERMISSIONS", "COURSE_AUDITOR", - "COURSE_AUDITOR_PERMISSIONS", "COURSE_BETA_TESTER", - "COURSE_BETA_TESTER_PERMISSIONS", "COURSE_DATA_RESEARCHER", - "COURSE_DATA_RESEARCHER_PERMISSIONS", "COURSE_EDITOR", - "COURSE_EDITOR_PERMISSIONS", "COURSE_LIMITED_STAFF", - "COURSE_LIMITED_STAFF_PERMISSIONS", "COURSE_STAFF", - "COURSE_STAFF_PERMISSIONS", "LEGACY_COURSE_ROLE_EQUIVALENCES", ] diff --git a/src/openedx_authz/tests/api/test_roles.py b/src/openedx_authz/tests/api/test_roles.py index 04372acf..6856fab4 100644 --- a/src/openedx_authz/tests/api/test_roles.py +++ b/src/openedx_authz/tests/api/test_roles.py @@ -42,16 +42,16 @@ unassign_subject_from_all_roles, ) from openedx_authz.constants import permissions, roles -from openedx_authz.constants.roles import ( +from openedx_authz.engine.enforcer import AuthzEnforcer +from openedx_authz.engine.utils import migrate_policy_between_enforcers +from openedx_authz.models import ExtendedCasbinRule, Scope, Subject +from openedx_authz.tests.role_permissions import ( COURSE_AUDITOR_PERMISSIONS, LIBRARY_ADMIN_PERMISSIONS, LIBRARY_AUTHOR_PERMISSIONS, LIBRARY_CONTRIBUTOR_PERMISSIONS, LIBRARY_USER_PERMISSIONS, ) -from openedx_authz.engine.enforcer import AuthzEnforcer -from openedx_authz.engine.utils import migrate_policy_between_enforcers -from openedx_authz.models import ExtendedCasbinRule, Scope, Subject def _mock_get_or_create_scope(scope_data): @@ -603,36 +603,21 @@ def test_get_subject_role_assignments_in_scope(self, subject_name, scope_name, e ( "alice", [ - RoleData( - external_key=roles.LIBRARY_ADMIN.external_key, - permissions=LIBRARY_ADMIN_PERMISSIONS, - ), + RoleData(external_key=roles.LIBRARY_ADMIN.external_key), ], ), ( "eve", [ - RoleData( - external_key=roles.LIBRARY_ADMIN.external_key, - permissions=LIBRARY_ADMIN_PERMISSIONS, - ), - RoleData( - external_key=roles.LIBRARY_AUTHOR.external_key, - permissions=LIBRARY_AUTHOR_PERMISSIONS, - ), - RoleData( - external_key=roles.LIBRARY_USER.external_key, - permissions=LIBRARY_USER_PERMISSIONS, - ), + RoleData(external_key=roles.LIBRARY_ADMIN.external_key), + RoleData(external_key=roles.LIBRARY_AUTHOR.external_key), + RoleData(external_key=roles.LIBRARY_USER.external_key), ], ), ( "frank", [ - RoleData( - external_key=roles.LIBRARY_USER.external_key, - permissions=LIBRARY_USER_PERMISSIONS, - ), + RoleData(external_key=roles.LIBRARY_USER.external_key), ], ), ("non_existent_user", []), @@ -1076,10 +1061,7 @@ def test_unassign_role_from_subject_in_scope(self, subject_names, role, scope_na RoleAssignmentData( subject=SubjectData(external_key="alice"), roles=[ - RoleData( - external_key=roles.LIBRARY_ADMIN.external_key, - permissions=LIBRARY_ADMIN_PERMISSIONS, - ) + RoleData(external_key=roles.LIBRARY_ADMIN.external_key) ], scope=ScopeData(external_key="lib:Org1:math_101"), ) @@ -1091,10 +1073,7 @@ def test_unassign_role_from_subject_in_scope(self, subject_names, role, scope_na RoleAssignmentData( subject=SubjectData(external_key="bob"), roles=[ - RoleData( - external_key=roles.LIBRARY_AUTHOR.external_key, - permissions=LIBRARY_AUTHOR_PERMISSIONS, - ) + RoleData(external_key=roles.LIBRARY_AUTHOR.external_key) ], scope=ScopeData(external_key="lib:Org1:history_201"), ) @@ -1106,10 +1085,7 @@ def test_unassign_role_from_subject_in_scope(self, subject_names, role, scope_na RoleAssignmentData( subject=SubjectData(external_key="carol"), roles=[ - RoleData( - external_key=roles.LIBRARY_CONTRIBUTOR.external_key, - permissions=LIBRARY_CONTRIBUTOR_PERMISSIONS, - ) + RoleData(external_key=roles.LIBRARY_CONTRIBUTOR.external_key) ], scope=ScopeData(external_key="lib:Org1:science_301"), ) @@ -1121,10 +1097,7 @@ def test_unassign_role_from_subject_in_scope(self, subject_names, role, scope_na RoleAssignmentData( subject=SubjectData(external_key="dave"), roles=[ - RoleData( - external_key=roles.LIBRARY_USER.external_key, - permissions=LIBRARY_USER_PERMISSIONS, - ) + RoleData(external_key=roles.LIBRARY_USER.external_key) ], scope=ScopeData(external_key="lib:Org1:english_101"), ) diff --git a/src/openedx_authz/tests/api/test_users.py b/src/openedx_authz/tests/api/test_users.py index e9ee99a5..605dff0c 100644 --- a/src/openedx_authz/tests/api/test_users.py +++ b/src/openedx_authz/tests/api/test_users.py @@ -38,7 +38,6 @@ validate_users, ) from openedx_authz.constants import permissions, roles -from openedx_authz.constants.roles import LIBRARY_ADMIN_PERMISSIONS, LIBRARY_AUTHOR_PERMISSIONS from openedx_authz.tests.api.test_roles import RolesTestSetupMixin @@ -268,10 +267,7 @@ def test_get_user_role_assignments_for_role_in_scope(self, role_name, scope_name RoleAssignmentData( subject=UserData(external_key="alice"), roles=[ - RoleData( - external_key=roles.LIBRARY_ADMIN.external_key, - permissions=LIBRARY_ADMIN_PERMISSIONS, - ) + RoleData(external_key=roles.LIBRARY_ADMIN.external_key) ], scope=ContentLibraryData(external_key="lib:Org1:math_101"), ), @@ -283,10 +279,7 @@ def test_get_user_role_assignments_for_role_in_scope(self, role_name, scope_name RoleAssignmentData( subject=UserData(external_key="bob"), roles=[ - RoleData( - external_key=roles.LIBRARY_AUTHOR.external_key, - permissions=LIBRARY_AUTHOR_PERMISSIONS, - ) + RoleData(external_key=roles.LIBRARY_AUTHOR.external_key) ], scope=ContentLibraryData(external_key="lib:Org1:history_201"), ), @@ -298,10 +291,7 @@ def test_get_user_role_assignments_for_role_in_scope(self, role_name, scope_name RoleAssignmentData( subject=UserData(external_key="eve"), roles=[ - RoleData( - external_key=roles.LIBRARY_ADMIN.external_key, - permissions=LIBRARY_ADMIN_PERMISSIONS, - ) + RoleData(external_key=roles.LIBRARY_ADMIN.external_key) ], scope=ContentLibraryData(external_key="lib:Org2:physics_401"), ), diff --git a/src/openedx_authz/tests/applets/test_builtin_definitions.py b/src/openedx_authz/tests/applets/test_builtin_definitions.py index 49a64ac2..a7d9c140 100644 --- a/src/openedx_authz/tests/applets/test_builtin_definitions.py +++ b/src/openedx_authz/tests/applets/test_builtin_definitions.py @@ -38,7 +38,7 @@ def test_compiled_schema_matches_expected_definitions(self): assert _schema_data(compiled) == expected def test_compiled_roles_match_public_role_constants(self): - """Every compiled role contains the permissions exposed by its public constant.""" + """Every compiled role has a public constant, and every constant names a compiled role.""" public_roles = ( roles.LIBRARY_ADMIN, roles.LIBRARY_AUTHOR, @@ -56,10 +56,6 @@ def test_compiled_roles_match_public_role_constants(self): compiled = SchemaPipeline().compile() assert set(compiled.roles) == {role.external_key for role in public_roles} - for role in public_roles: - assert set(compiled.roles[role.external_key].definition.permissions) == set( - role.get_permission_identifiers() - ) def test_compiled_sources_belong_to_their_owning_applets(self): """Compiled definitions expose their owning applet through source metadata.""" diff --git a/src/openedx_authz/tests/rest_api/test_views.py b/src/openedx_authz/tests/rest_api/test_views.py index 4342f828..6eee2e9b 100644 --- a/src/openedx_authz/tests/rest_api/test_views.py +++ b/src/openedx_authz/tests/rest_api/test_views.py @@ -29,6 +29,7 @@ from openedx_authz.rest_api.v1.permissions import AnyScopePermission, DynamicScopePermission from openedx_authz.rest_api.v1.views import UserValidationAPIView from openedx_authz.tests.rest_api.mixins import ViewTestMixin +from openedx_authz.tests.role_permissions import LIBRARY_ADMIN_PERMISSIONS from openedx_authz.tests.test_utils import make_action_key, make_role_key, make_wildcard_key User = get_user_model() @@ -105,7 +106,7 @@ def test_permission_validation_staff_superuser_access(self, scope: str, expected - For non-library scopes: Permissions follow normal authorization (False) """ self.client.force_authenticate(user=self.admin_user) - request_data = [{"action": perm.identifier, "scope": scope} for perm in roles.LIBRARY_ADMIN_PERMISSIONS] + request_data = [{"action": perm.identifier, "scope": scope} for perm in LIBRARY_ADMIN_PERMISSIONS] expected_response = request_data.copy() for item in expected_response: item["allowed"] = expected_result diff --git a/src/openedx_authz/tests/role_permissions.py b/src/openedx_authz/tests/role_permissions.py new file mode 100644 index 00000000..1aceb398 --- /dev/null +++ b/src/openedx_authz/tests/role_permissions.py @@ -0,0 +1,61 @@ +"""Expected permissions for built-in roles, as defined in the applets' YAML schema files.""" + +from openedx_authz.constants import permissions + +LIBRARY_ADMIN_PERMISSIONS = [ + permissions.VIEW_LIBRARY, + permissions.MANAGE_LIBRARY_TAGS, + permissions.DELETE_LIBRARY, + permissions.EDIT_LIBRARY_CONTENT, + permissions.PUBLISH_LIBRARY_CONTENT, + permissions.REUSE_LIBRARY_CONTENT, + permissions.VIEW_LIBRARY_TEAM, + permissions.MANAGE_LIBRARY_TEAM, + permissions.CREATE_LIBRARY_COLLECTION, + permissions.EDIT_LIBRARY_COLLECTION, + permissions.DELETE_LIBRARY_COLLECTION, +] + +LIBRARY_AUTHOR_PERMISSIONS = [ + permissions.VIEW_LIBRARY, + permissions.MANAGE_LIBRARY_TAGS, + permissions.EDIT_LIBRARY_CONTENT, + permissions.PUBLISH_LIBRARY_CONTENT, + permissions.REUSE_LIBRARY_CONTENT, + permissions.VIEW_LIBRARY_TEAM, + permissions.CREATE_LIBRARY_COLLECTION, + permissions.EDIT_LIBRARY_COLLECTION, + permissions.DELETE_LIBRARY_COLLECTION, +] + +LIBRARY_CONTRIBUTOR_PERMISSIONS = [ + permissions.VIEW_LIBRARY, + permissions.MANAGE_LIBRARY_TAGS, + permissions.EDIT_LIBRARY_CONTENT, + permissions.REUSE_LIBRARY_CONTENT, + permissions.VIEW_LIBRARY_TEAM, + permissions.CREATE_LIBRARY_COLLECTION, + permissions.EDIT_LIBRARY_COLLECTION, + permissions.DELETE_LIBRARY_COLLECTION, +] + +LIBRARY_USER_PERMISSIONS = [ + permissions.VIEW_LIBRARY, + permissions.REUSE_LIBRARY_CONTENT, + permissions.VIEW_LIBRARY_TEAM, +] + +COURSE_AUDITOR_PERMISSIONS = [ + permissions.COURSES_VIEW_COURSE, + permissions.COURSES_VIEW_COURSE_UPDATES, + permissions.COURSES_VIEW_PAGES_AND_RESOURCES, + permissions.COURSES_VIEW_FILES, + permissions.COURSES_VIEW_GRADING_SETTINGS, + 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, +] From 5e835f22e65e24d5c7b772dc75db5d0010623b1d Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 20:30:22 +0200 Subject: [PATCH 04/11] fix: remove the courses.create_course permission constant Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.rst | 1 + .../core_roles_and_permissions/course_roles.rst | 2 -- .../applets/course_authoring/constants.py | 6 ------ .../tests/applets/test_builtin_definitions.py | 15 ++++++++++++++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 447f57b9..055d8b90 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -62,6 +62,7 @@ Removed ======= * Removed the role permission lists from ``openedx_authz.constants.roles``, such as ``COURSE_ADMIN_PERMISSIONS`` and ``LIBRARY_ADMIN_PERMISSIONS``. Role constants now identify a role and have an empty ``permissions`` list. Use ``openedx_authz.api.get_permissions_for_single_role`` or the compiled schema to read a role's permissions. +* Removed ``COURSES_CREATE_COURSE`` (``courses.create_course``) from ``openedx_authz.constants.permissions``. No schema file defined it, so no role could include it. Fixed ===== diff --git a/docs/concepts/core_roles_and_permissions/course_roles.rst b/docs/concepts/core_roles_and_permissions/course_roles.rst index 2a9b4d13..dad7c5f9 100644 --- a/docs/concepts/core_roles_and_permissions/course_roles.rst +++ b/docs/concepts/core_roles_and_permissions/course_roles.rst @@ -36,7 +36,6 @@ The following permissions are associated with the course roles: Course Access & Content ========================= - **View course** (``courses.view_course``): Allows users to view the course and access the course outline in read-only mode. -- **Create course** (``courses.create_course``): Allows users to create a new course in Studio. - **Edit course content** (``courses.edit_course_content``): Allows users to edit course content, outline, units, and components. - **Publish course content** (``courses.publish_course_content``): Allows users to publish course content. @@ -132,7 +131,6 @@ Roles and Permissions Summary Table **Access & Content** --------------------------------------------- -------------- -------------- --------------------- -------------- courses.view_course ✅ ✅ ✅ ✅ - courses.create_course ❌ ❌ ❌ ❌ courses.publish_course_content ✅ ✅ ❌ ❌ courses.edit_course_content ✅ ✅ ✅ ❌ **Files** diff --git a/src/openedx_authz/applets/course_authoring/constants.py b/src/openedx_authz/applets/course_authoring/constants.py index 2d5fce92..b3398cfd 100644 --- a/src/openedx_authz/applets/course_authoring/constants.py +++ b/src/openedx_authz/applets/course_authoring/constants.py @@ -7,7 +7,6 @@ "COURSES_VIEW_COURSE", "COURSES_VIEW_COURSE_TEAM", "COURSES_MANAGE_COURSE_TEAM", - "COURSES_CREATE_COURSE", "COURSES_EDIT_COURSE_CONTENT", "COURSES_PUBLISH_COURSE_CONTENT", "COURSES_VIEW_LIBRARY_UPDATES", @@ -56,11 +55,6 @@ effect="allow", ) -COURSES_CREATE_COURSE = PermissionData( - action=ActionData(external_key=f"{COURSES_NAMESPACE}.create_course"), - effect="allow", -) - COURSES_EDIT_COURSE_CONTENT = PermissionData( action=ActionData(external_key=f"{COURSES_NAMESPACE}.edit_course_content"), effect="allow", diff --git a/src/openedx_authz/tests/applets/test_builtin_definitions.py b/src/openedx_authz/tests/applets/test_builtin_definitions.py index a7d9c140..c4c737ad 100644 --- a/src/openedx_authz/tests/applets/test_builtin_definitions.py +++ b/src/openedx_authz/tests/applets/test_builtin_definitions.py @@ -4,7 +4,8 @@ from dataclasses import asdict from pathlib import Path -from openedx_authz.constants import roles +from openedx_authz.constants import permissions, roles +from openedx_authz.data import PermissionData from openedx_authz.engine.schema.pipeline import SchemaPipeline COMPILED_SCHEMA_FIXTURE = Path(__file__).with_name("fixtures") / "compiled_schema.json" @@ -57,6 +58,18 @@ def test_compiled_roles_match_public_role_constants(self): assert set(compiled.roles) == {role.external_key for role in public_roles} + def test_compiled_permissions_match_public_permission_constants(self): + """Every compiled permission has a public constant, and every constant names a compiled permission.""" + public_permissions = { + constant.identifier + for constant in (getattr(permissions, name) for name in permissions.__all__) + if isinstance(constant, PermissionData) + } + + compiled = SchemaPipeline().compile() + + assert set(compiled.permissions) == public_permissions + def test_compiled_sources_belong_to_their_owning_applets(self): """Compiled definitions expose their owning applet through source metadata.""" compiled = SchemaPipeline().compile() From f6f0f9bc14da5c7faf433020d5ca00136709ea11 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 20:37:39 +0200 Subject: [PATCH 05/11] refactor: define course-team roles directly as RoleData Co-Authored-By: Claude Opus 5.5 --- .../applets/course_team_roles/constants.py | 10 +------- .../applets/course_team_roles/roles.py | 23 ++++++------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/openedx_authz/applets/course_team_roles/constants.py b/src/openedx_authz/applets/course_team_roles/constants.py index 77139a31..8703c5bd 100644 --- a/src/openedx_authz/applets/course_team_roles/constants.py +++ b/src/openedx_authz/applets/course_team_roles/constants.py @@ -1,4 +1,4 @@ -"""Course-team role identifiers and legacy compatibility permissions.""" +"""Legacy compatibility permissions for course-team roles.""" from openedx_authz.data import ActionData, PermissionData @@ -27,11 +27,3 @@ COURSES_LEGACY_BETA_TESTER_PERMISSIONS = PermissionData( action=ActionData(external_key=f"{COURSES_NAMESPACE}.legacy_beta_tester_permissions"), effect="allow" ) - -COURSE_ADMIN_ID = "course_admin" -COURSE_STAFF_ID = "course_staff" -COURSE_EDITOR_ID = "course_editor" -COURSE_AUDITOR_ID = "course_auditor" -COURSE_LIMITED_STAFF_ID = "course_limited_staff" -COURSE_DATA_RESEARCHER_ID = "course_data_researcher" -COURSE_BETA_TESTER_ID = "course_beta_tester" diff --git a/src/openedx_authz/applets/course_team_roles/roles.py b/src/openedx_authz/applets/course_team_roles/roles.py index aaf3363d..1e422c75 100644 --- a/src/openedx_authz/applets/course_team_roles/roles.py +++ b/src/openedx_authz/applets/course_team_roles/roles.py @@ -5,23 +5,14 @@ """ from openedx_authz.api.data import RoleData -from openedx_authz.applets.course_team_roles.constants import ( - COURSE_ADMIN_ID, - COURSE_AUDITOR_ID, - COURSE_BETA_TESTER_ID, - COURSE_DATA_RESEARCHER_ID, - COURSE_EDITOR_ID, - COURSE_LIMITED_STAFF_ID, - COURSE_STAFF_ID, -) -COURSE_AUDITOR = RoleData(external_key=COURSE_AUDITOR_ID) -COURSE_EDITOR = RoleData(external_key=COURSE_EDITOR_ID) -COURSE_ADMIN = RoleData(external_key=COURSE_ADMIN_ID) -COURSE_STAFF = RoleData(external_key=COURSE_STAFF_ID) -COURSE_LIMITED_STAFF = RoleData(external_key=COURSE_LIMITED_STAFF_ID) -COURSE_DATA_RESEARCHER = RoleData(external_key=COURSE_DATA_RESEARCHER_ID) -COURSE_BETA_TESTER = RoleData(external_key=COURSE_BETA_TESTER_ID) +COURSE_AUDITOR = RoleData(external_key="course_auditor") +COURSE_EDITOR = RoleData(external_key="course_editor") +COURSE_ADMIN = RoleData(external_key="course_admin") +COURSE_STAFF = RoleData(external_key="course_staff") +COURSE_LIMITED_STAFF = RoleData(external_key="course_limited_staff") +COURSE_DATA_RESEARCHER = RoleData(external_key="course_data_researcher") +COURSE_BETA_TESTER = RoleData(external_key="course_beta_tester") # This is a known LMS-only permission, but doesn't actually grant anything yet. # From 833fd5e412856183c8854f8bfe6479fb2a653cba Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 20:45:21 +0200 Subject: [PATCH 06/11] chore: revert changes unrelated to the applet move Co-Authored-By: Claude Opus 5.5 --- .../course_authoring/schema/permissions.yaml | 16 +++++----- .../schema/role_extensions.yaml | 10 +++---- src/openedx_authz/authz/__init__.py | 7 +++-- src/openedx_authz/engine/renderer.py | 12 +++----- src/openedx_authz/engine/schema/discovery.py | 2 +- src/openedx_authz/tests/schema/test_apply.py | 1 + .../tests/schema/test_discovery.py | 29 ++++++++++++------- 7 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/openedx_authz/applets/course_authoring/schema/permissions.yaml b/src/openedx_authz/applets/course_authoring/schema/permissions.yaml index d01d558e..8fa7c868 100644 --- a/src/openedx_authz/applets/course_authoring/schema/permissions.yaml +++ b/src/openedx_authz/applets/course_authoring/schema/permissions.yaml @@ -201,14 +201,6 @@ permissions: scopes: - course-v1 icon: EditOutline - - namespace: courses - name: view_group_configurations - display_name: View group configurations - description: See the list of content groups and their configurations for this course. - category_id: course_team_group - scopes: - - course-v1 - icon: RemoveRedEye - namespace: courses name: view_course_team display_name: View course team @@ -225,6 +217,14 @@ permissions: scopes: - course-v1 icon: Settings + - namespace: courses + name: view_group_configurations + display_name: View group configurations + description: See the list of content groups and their configurations for this course. + category_id: course_team_group + scopes: + - course-v1 + icon: RemoveRedEye - namespace: courses name: manage_group_configurations display_name: Manage group configurations diff --git a/src/openedx_authz/applets/course_authoring/schema/role_extensions.yaml b/src/openedx_authz/applets/course_authoring/schema/role_extensions.yaml index 6c5645b0..29b35790 100644 --- a/src/openedx_authz/applets/course_authoring/schema/role_extensions.yaml +++ b/src/openedx_authz/applets/course_authoring/schema/role_extensions.yaml @@ -10,11 +10,11 @@ role_extensions: - courses.view_files - courses.view_grading_settings - courses.view_checklists + - courses.view_course_team - courses.view_schedule_and_details - courses.view_advanced_settings - courses.view_certificates - courses.view_group_configurations - - courses.view_course_team - courses.view_library_updates - courses.edit_course_content - courses.manage_library_updates @@ -24,7 +24,6 @@ role_extensions: - courses.edit_files - courses.edit_grading_settings - courses.manage_group_configurations - - courses.manage_course_team - courses.edit_details - courses.manage_tags - courses.publish_course_content @@ -35,6 +34,7 @@ role_extensions: - courses.import_course - courses.export_course - courses.export_tags + - courses.manage_course_team - courses.manage_taxonomies - role_id: course_staff @@ -45,11 +45,11 @@ role_extensions: - courses.view_files - courses.view_grading_settings - courses.view_checklists + - courses.view_course_team - courses.view_schedule_and_details - courses.view_advanced_settings - courses.view_certificates - courses.view_group_configurations - - courses.view_course_team - courses.view_library_updates - courses.edit_course_content - courses.manage_library_updates @@ -78,11 +78,11 @@ role_extensions: - courses.view_files - courses.view_grading_settings - courses.view_checklists + - courses.view_course_team - courses.view_schedule_and_details - courses.view_advanced_settings - courses.view_certificates - courses.view_group_configurations - - courses.view_course_team - courses.view_library_updates - courses.edit_course_content - courses.manage_library_updates @@ -103,9 +103,9 @@ role_extensions: - courses.view_files - courses.view_grading_settings - courses.view_checklists + - courses.view_course_team - courses.view_schedule_and_details - courses.view_advanced_settings - courses.view_certificates - courses.view_group_configurations - - courses.view_course_team - courses.view_library_updates diff --git a/src/openedx_authz/authz/__init__.py b/src/openedx_authz/authz/__init__.py index 14ce435d..d2640ce8 100644 --- a/src/openedx_authz/authz/__init__.py +++ b/src/openedx_authz/authz/__init__.py @@ -1,9 +1,10 @@ """openedx-authz's own static authorization schema resources. This package exposes the built-in applet schema directories through the -``authz.schema`` entry point (ADR 0019). Openedx-authz is a schema provider like -any other distribution, and its directories are discovered the same way a -third-party application's would be. +``authz.schema`` entry point (ADR 0019). openedx-authz is a schema provider like +any other distribution; its directories are discovered the same way a +third-party application's would be, and the loader reads every ``.yaml`` file +inside them. Register in setup.py / pyproject.toml:: diff --git a/src/openedx_authz/engine/renderer.py b/src/openedx_authz/engine/renderer.py index c95a96ee..c580914f 100644 --- a/src/openedx_authz/engine/renderer.py +++ b/src/openedx_authz/engine/renderer.py @@ -587,9 +587,8 @@ def source_obj(record): ) category_objs[cid] = obj for record in compiled.sources: - source = source_obj(record) schema_models.AuthzCategorySource.objects.update_or_create( - category=obj, source=source, defaults={"origin_kind": schema_models.OriginKind.BASE} + category=obj, source=source_obj(record), defaults={"origin_kind": schema_models.OriginKind.BASE} ) # Permissions. @@ -609,9 +608,8 @@ def source_obj(record): ) permission_objs[pid] = obj for record in compiled.sources: - source = source_obj(record) schema_models.AuthzPermissionSource.objects.update_or_create( - permission=obj, source=source, defaults={"origin_kind": schema_models.OriginKind.BASE} + permission=obj, source=source_obj(record), defaults={"origin_kind": schema_models.OriginKind.BASE} ) # Roles. @@ -630,9 +628,8 @@ def source_obj(record): ) role_objs[rid] = obj for record in compiled.sources: - source = source_obj(record) schema_models.AuthzRoleSource.objects.update_or_create( - role=obj, source=source, defaults={"origin_kind": schema_models.OriginKind.BASE} + role=obj, source=source_obj(record), defaults={"origin_kind": schema_models.OriginKind.BASE} ) # Role-permission grants (one per rendered role/permission/scope triple). @@ -652,10 +649,9 @@ def source_obj(record): ) live_grant_ids.add(grant.pk) for rel in schema.role_permission_sources.get((rid, perm_id), []): - source = source_obj(rel.source) schema_models.AuthzRolePermissionSource.objects.update_or_create( role_permission=grant, - source=source, + source=source_obj(rel.source), defaults={"origin_kind": rel.origin_kind, "priority": rel.priority}, ) diff --git a/src/openedx_authz/engine/schema/discovery.py b/src/openedx_authz/engine/schema/discovery.py index efc6d8c0..9cb0c5a8 100644 --- a/src/openedx_authz/engine/schema/discovery.py +++ b/src/openedx_authz/engine/schema/discovery.py @@ -4,7 +4,7 @@ every ``.yaml`` file inside them. Two contribution sources are merged: 1. The ``authz.schema`` entry-point group. Each registered callable returns - directory paths in the package-anchored format below (for example, + directory paths in the package-anchored format below (e.g. ``["openedx_authz/applets/content_libraries/schema"]``). 2. The ``OPENEDX_AUTHZ_SCHEMA_DIRECTORIES`` Django setting, a list of directory path strings in the same format. This lets operators and CI contribute diff --git a/src/openedx_authz/tests/schema/test_apply.py b/src/openedx_authz/tests/schema/test_apply.py index 6efc2caf..423d9023 100644 --- a/src/openedx_authz/tests/schema/test_apply.py +++ b/src/openedx_authz/tests/schema/test_apply.py @@ -454,6 +454,7 @@ def test_removed_role_and_category_are_pruned(self): assert AuthzPermissionDefinition.objects.count() == 0 assert AuthzPermissionCategory.objects.count() == 0 + @pytest.mark.django_db @pytest.mark.usefixtures("cache_invalidation") class TestDefinitionChangeReport: diff --git a/src/openedx_authz/tests/schema/test_discovery.py b/src/openedx_authz/tests/schema/test_discovery.py index 8e22f6ab..2c523f14 100644 --- a/src/openedx_authz/tests/schema/test_discovery.py +++ b/src/openedx_authz/tests/schema/test_discovery.py @@ -22,6 +22,7 @@ ) SCHEMA_DIR = "openedx_authz/applets/content_libraries/schema" +SCHEMA_MODULE = "openedx_authz.applets.content_libraries.schema" EXPECTED_FILES = {"permissions.yaml", "roles.yaml"} EXPECTED_BUILTIN_PATHS = { "applets/content_libraries/schema/permissions.yaml", @@ -39,27 +40,33 @@ class TestDirectoryExpansion: def test_directory_is_expanded_to_yaml_files(self): """Passing a directory yields exactly its YAML files, by name.""" - resources = SchemaDiscovery()._iter_directory( # pylint: disable=protected-access - SCHEMA_DIR, origin=Origin.PASSED_IN - ) + resources = [ + resource + for resource in SchemaDiscovery(passed_in_directories=[SCHEMA_DIR]).discover() + if resource.module == SCHEMA_MODULE + ] names = {r.resource_path.rsplit("/", 1)[-1] for r in resources} assert names == EXPECTED_FILES def test_discovered_resource_anchors_and_module_are_set(self): """Each resource records its import anchor, resource path, and module.""" - resources = SchemaDiscovery()._iter_directory( # pylint: disable=protected-access - SCHEMA_DIR, origin=Origin.PASSED_IN - ) + resources = [ + resource + for resource in SchemaDiscovery(passed_in_directories=[SCHEMA_DIR]).discover() + if resource.module == SCHEMA_MODULE + ] sample = resources[0] assert sample.package == "openedx_authz" # importable anchor assert sample.resource_path.startswith("applets/content_libraries/schema/") - assert sample.module == "openedx_authz.applets.content_libraries.schema" # source identity + assert sample.module == SCHEMA_MODULE # source identity def test_contents_are_readable(self): """A discovered resource can be read back as non-empty bytes.""" - resources = SchemaDiscovery()._iter_directory( # pylint: disable=protected-access - SCHEMA_DIR, origin=Origin.PASSED_IN - ) + resources = [ + resource + for resource in SchemaDiscovery(passed_in_directories=[SCHEMA_DIR]).discover() + if resource.module == SCHEMA_MODULE + ] assert resources[0].read_bytes() # non-empty bytes def test_discovery_is_deterministic(self): @@ -85,7 +92,7 @@ def test_duplicate_directories_are_deduplicated(self): content_library_paths = [ resource.resource_path for resource in resources - if resource.module == "openedx_authz.applets.content_libraries.schema" + if resource.module == SCHEMA_MODULE ] assert len(content_library_paths) == len(set(content_library_paths)) == len(EXPECTED_FILES) From 5f7002a442e707e9e976e7b894051814e91415af Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 20:45:21 +0200 Subject: [PATCH 07/11] refactor: re-export applet constants with one import per module Co-Authored-By: Claude Opus 5.5 --- src/openedx_authz/constants/permissions.py | 9 --------- src/openedx_authz/constants/roles.py | 7 ------- .../tests/applets/test_builtin_definitions.py | 4 +--- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/openedx_authz/constants/permissions.py b/src/openedx_authz/constants/permissions.py index 1c8a821b..d58b9bbf 100644 --- a/src/openedx_authz/constants/permissions.py +++ b/src/openedx_authz/constants/permissions.py @@ -2,15 +2,6 @@ # pylint: disable=wildcard-import,unused-wildcard-import -from openedx_authz.applets.content_libraries import constants as content_libraries from openedx_authz.applets.content_libraries.constants import * # noqa: F403 -from openedx_authz.applets.course_authoring import constants as course_authoring from openedx_authz.applets.course_authoring.constants import * # noqa: F403 -from openedx_authz.applets.course_team_roles import constants as course_team_roles from openedx_authz.applets.course_team_roles.constants import * # noqa: F403 - -__all__ = [ - *content_libraries.__all__, - *course_authoring.__all__, - *course_team_roles.__all__, -] diff --git a/src/openedx_authz/constants/roles.py b/src/openedx_authz/constants/roles.py index 2845df34..ab3947fd 100644 --- a/src/openedx_authz/constants/roles.py +++ b/src/openedx_authz/constants/roles.py @@ -2,12 +2,5 @@ # pylint: disable=wildcard-import,unused-wildcard-import -from openedx_authz.applets.content_libraries import roles as content_libraries from openedx_authz.applets.content_libraries.roles import * # noqa: F403 -from openedx_authz.applets.course_team_roles import roles as course_team_roles from openedx_authz.applets.course_team_roles.roles import * # noqa: F403 - -__all__ = [ - *content_libraries.__all__, - *course_team_roles.__all__, -] diff --git a/src/openedx_authz/tests/applets/test_builtin_definitions.py b/src/openedx_authz/tests/applets/test_builtin_definitions.py index c4c737ad..aebf4716 100644 --- a/src/openedx_authz/tests/applets/test_builtin_definitions.py +++ b/src/openedx_authz/tests/applets/test_builtin_definitions.py @@ -61,9 +61,7 @@ def test_compiled_roles_match_public_role_constants(self): def test_compiled_permissions_match_public_permission_constants(self): """Every compiled permission has a public constant, and every constant names a compiled permission.""" public_permissions = { - constant.identifier - for constant in (getattr(permissions, name) for name in permissions.__all__) - if isinstance(constant, PermissionData) + constant.identifier for constant in vars(permissions).values() if isinstance(constant, PermissionData) } compiled = SchemaPipeline().compile() From a52c16d45fd3f7a254fbc3014f374a04798eb844 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 20:45:21 +0200 Subject: [PATCH 08/11] docs: keep one line per paragraph in applet READMEs and changelog Co-Authored-By: Claude Opus 5.5 --- src/openedx_authz/applets/course_authoring/README.rst | 8 ++------ src/openedx_authz/applets/course_team_roles/README.rst | 4 +++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/openedx_authz/applets/course_authoring/README.rst b/src/openedx_authz/applets/course_authoring/README.rst index a2c9b107..197993fb 100644 --- a/src/openedx_authz/applets/course_authoring/README.rst +++ b/src/openedx_authz/applets/course_authoring/README.rst @@ -1,10 +1,6 @@ Course Authoring ################ -This applet owns the existing built-in permissions for authoring, configuring, -and managing the team of a course. It adds those permissions to the shared -course-team roles through role extensions, while the role identifiers and -metadata remain in ``course_team_roles``. +This applet owns the existing built-in permissions for authoring, configuring, and managing the team of a course. It adds those permissions to the shared course-team roles through role extensions, while the role identifiers and metadata remain in ``course_team_roles``. ``constants.py`` contains its permission objects. -The applet is an internal package. Existing consumers should continue importing -permission and role constants from ``openedx_authz.constants``. +The applet is an internal package, so clients import these constants from ``openedx_authz.constants.permissions``, which re-exports the objects defined here. diff --git a/src/openedx_authz/applets/course_team_roles/README.rst b/src/openedx_authz/applets/course_team_roles/README.rst index c2e45910..0a105ad2 100644 --- a/src/openedx_authz/applets/course_team_roles/README.rst +++ b/src/openedx_authz/applets/course_team_roles/README.rst @@ -3,4 +3,6 @@ Course-Team Roles This applet owns the identifiers and metadata for roles held by members of a course team, including the legacy compatibility roles. Their base definitions have empty permission lists because the applets that own each permission add it through role extensions. -The legacy compatibility permissions also live here because they connect these roles to the previous course-role system. ``roles.py`` contains the role constants, which identify each role without listing its permissions. Existing consumers continue importing them from ``openedx_authz.constants``. +The legacy compatibility permissions also live here because they connect these roles to the previous course-role system. ``constants.py`` contains these permission objects, and ``roles.py`` contains the role constants, which identify each role without listing its permissions. + +The applet is an internal package, so clients import these constants from ``openedx_authz.constants.permissions`` and ``openedx_authz.constants.roles``, which re-export the objects defined here. From 7ccff09930d878bfdeba08ec95dfa3c19a16670d Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 21:03:36 +0200 Subject: [PATCH 09/11] chore: keep only the lint suppression the re-export modules need Co-Authored-By: Claude Opus 5.5 --- .../schema/role_extensions.yaml | 19 ------------------- src/openedx_authz/constants/permissions.py | 8 ++++---- src/openedx_authz/constants/roles.py | 6 +++--- 3 files changed, 7 insertions(+), 26 deletions(-) delete mode 100644 src/openedx_authz/applets/course_team_roles/schema/role_extensions.yaml diff --git a/src/openedx_authz/applets/course_team_roles/schema/role_extensions.yaml b/src/openedx_authz/applets/course_team_roles/schema/role_extensions.yaml deleted file mode 100644 index d07cea31..00000000 --- a/src/openedx_authz/applets/course_team_roles/schema/role_extensions.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" -priority: 100 - -role_extensions: - - role_id: course_admin - add_permissions: - - courses.legacy_instructor_role_permissions - - role_id: course_staff - add_permissions: - - courses.legacy_staff_role_permissions - - role_id: course_limited_staff - add_permissions: - - courses.legacy_limited_staff_role_permissions - - role_id: course_data_researcher - add_permissions: - - courses.legacy_data_researcher_permissions - - role_id: course_beta_tester - add_permissions: - - courses.legacy_beta_tester_permissions diff --git a/src/openedx_authz/constants/permissions.py b/src/openedx_authz/constants/permissions.py index d58b9bbf..1f1df2b0 100644 --- a/src/openedx_authz/constants/permissions.py +++ b/src/openedx_authz/constants/permissions.py @@ -1,7 +1,7 @@ """Compatibility exports for built-in permission constants.""" -# pylint: disable=wildcard-import,unused-wildcard-import +# pylint: disable=wildcard-import -from openedx_authz.applets.content_libraries.constants import * # noqa: F403 -from openedx_authz.applets.course_authoring.constants import * # noqa: F403 -from openedx_authz.applets.course_team_roles.constants import * # noqa: F403 +from openedx_authz.applets.content_libraries.constants import * +from openedx_authz.applets.course_authoring.constants import * +from openedx_authz.applets.course_team_roles.constants import * diff --git a/src/openedx_authz/constants/roles.py b/src/openedx_authz/constants/roles.py index ab3947fd..a43306ce 100644 --- a/src/openedx_authz/constants/roles.py +++ b/src/openedx_authz/constants/roles.py @@ -1,6 +1,6 @@ """Compatibility exports for built-in role constants.""" -# pylint: disable=wildcard-import,unused-wildcard-import +# pylint: disable=wildcard-import -from openedx_authz.applets.content_libraries.roles import * # noqa: F403 -from openedx_authz.applets.course_team_roles.roles import * # noqa: F403 +from openedx_authz.applets.content_libraries.roles import * +from openedx_authz.applets.course_team_roles.roles import * From f795a380a6e9dce6ec2ea9c8c3b41509109f8afd Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 21:03:36 +0200 Subject: [PATCH 10/11] refactor: list legacy permissions in the course-team role definitions Co-Authored-By: Claude Opus 5.5 --- .../applets/course_team_roles/README.rst | 2 +- .../applets/course_team_roles/schema/roles.yaml | 15 ++++++++++----- src/openedx_authz/tests/schema/test_discovery.py | 1 - 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/openedx_authz/applets/course_team_roles/README.rst b/src/openedx_authz/applets/course_team_roles/README.rst index 0a105ad2..da7047e2 100644 --- a/src/openedx_authz/applets/course_team_roles/README.rst +++ b/src/openedx_authz/applets/course_team_roles/README.rst @@ -1,7 +1,7 @@ Course-Team Roles ################# -This applet owns the identifiers and metadata for roles held by members of a course team, including the legacy compatibility roles. Their base definitions have empty permission lists because the applets that own each permission add it through role extensions. +This applet owns the identifiers and metadata for roles held by members of a course team, including the legacy compatibility roles. Their base definitions list only the legacy compatibility permissions, and the applets that own every other permission add it through role extensions. The legacy compatibility permissions also live here because they connect these roles to the previous course-role system. ``constants.py`` contains these permission objects, and ``roles.py`` contains the role constants, which identify each role without listing its permissions. diff --git a/src/openedx_authz/applets/course_team_roles/schema/roles.yaml b/src/openedx_authz/applets/course_team_roles/schema/roles.yaml index 04d04455..a5aed467 100644 --- a/src/openedx_authz/applets/course_team_roles/schema/roles.yaml +++ b/src/openedx_authz/applets/course_team_roles/schema/roles.yaml @@ -7,14 +7,16 @@ roles: description: Can manage the course team and all course settings. scopes: - course-v1 - permissions: [] + permissions: + - courses.legacy_instructor_role_permissions - id: course_staff display_name: Course Staff description: Can publish content and manage the course lifecycle in Studio. scopes: - course-v1 - permissions: [] + permissions: + - courses.legacy_staff_role_permissions - id: course_editor display_name: Course Editor @@ -38,7 +40,8 @@ roles: scopes: - course-v1 hidden: true - permissions: [] + permissions: + - courses.legacy_limited_staff_role_permissions - id: course_data_researcher display_name: Course Data Researcher (legacy) @@ -46,7 +49,8 @@ roles: scopes: - course-v1 hidden: true - permissions: [] + permissions: + - courses.legacy_data_researcher_permissions - id: course_beta_tester display_name: Course Beta Tester (legacy) @@ -54,4 +58,5 @@ roles: scopes: - course-v1 hidden: true - permissions: [] + permissions: + - courses.legacy_beta_tester_permissions diff --git a/src/openedx_authz/tests/schema/test_discovery.py b/src/openedx_authz/tests/schema/test_discovery.py index 2c523f14..76dfff0b 100644 --- a/src/openedx_authz/tests/schema/test_discovery.py +++ b/src/openedx_authz/tests/schema/test_discovery.py @@ -30,7 +30,6 @@ "applets/course_authoring/schema/permissions.yaml", "applets/course_authoring/schema/role_extensions.yaml", "applets/course_team_roles/schema/legacy_permissions.yaml", - "applets/course_team_roles/schema/role_extensions.yaml", "applets/course_team_roles/schema/roles.yaml", } From 75f06378f7948b8a09cf4ca59253ab02bd3011d1 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 21 Oct 2026 21:42:36 +0200 Subject: [PATCH 11/11] docs: publish the compiled schema as a readable snapshot Co-Authored-By: Claude Opus 5.5 --- Makefile | 5 +- docs/references/compiled-schema.rst | 11 + docs/references/compiled-schema.yaml | 803 +++++++++ docs/references/index.rst | 1 + scripts/generate_compiled_schema.py | 35 + .../applets/fixtures/compiled_schema.json | 1432 ----------------- .../tests/applets/test_builtin_definitions.py | 63 +- 7 files changed, 898 insertions(+), 1452 deletions(-) create mode 100644 docs/references/compiled-schema.rst create mode 100644 docs/references/compiled-schema.yaml create mode 100644 scripts/generate_compiled_schema.py delete mode 100644 src/openedx_authz/tests/applets/fixtures/compiled_schema.json diff --git a/Makefile b/Makefile index d5789ce6..ae9030ef 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: clean clean_tox compile_translations coverage diff_cover docs dummy_translations \ - extract_translations fake_translations help pii_check pull_translations paragon_icons \ + extract_translations fake_translations help pii_check pull_translations paragon_icons compiled_schema \ paragon_icons_check quality requirements selfcheck test test-all upgrade validate \ install_transifex_client @@ -43,6 +43,9 @@ paragon_icons: ## regenerate the vendored @openedx/paragon icon-name allow-list paragon_icons_check: ## fail if the vendored Paragon icon list is out of date (CI) python scripts/generate_paragon_icons.py --check +compiled_schema: ## regenerate the compiled schema snapshot in docs/references/compiled-schema.yaml + python scripts/generate_compiled_schema.py + quality: ## check coding style with pycodestyle and pylint tox -e quality diff --git a/docs/references/compiled-schema.rst b/docs/references/compiled-schema.rst new file mode 100644 index 00000000..3d65e9bf --- /dev/null +++ b/docs/references/compiled-schema.rst @@ -0,0 +1,11 @@ +.. _Compiled Authorization Schema: + +Compiled Authorization Schema +############################# + +The built-in applets each contribute part of the authorization schema, so a role's complete permission list only exists after the schema pipeline composes the base definitions with the ``role_extensions`` from every applet. This page shows that compiled result for the built-in applets, where each role lists its permissions grouped by the applet that adds them, and each permission and category names the applet that defines it. Installations may add more definitions through their own schema contributions, which this snapshot does not include. + +The snapshot is generated from the applet schemas with ``make compiled_schema``, and a test fails when it no longer matches the compiled schema, so any change to an applet schema also shows its effect on the compiled roles in the same diff. You can also :download:`download the snapshot `. + +.. literalinclude:: compiled-schema.yaml + :language: yaml diff --git a/docs/references/compiled-schema.yaml b/docs/references/compiled-schema.yaml new file mode 100644 index 00000000..355b3f27 --- /dev/null +++ b/docs/references/compiled-schema.yaml @@ -0,0 +1,803 @@ +# Compiled authorization schema of the built-in applets, generated by `make compiled_schema`. +# Each role lists its permissions grouped by the applet that adds them. +roles: + course_admin: + display_name: Course Admin + description: Can manage the course team and all course settings. + scopes: + - course-v1 + icon: null + hidden: false + permissions: + course_authoring: + - courses.create_files + - courses.delete_files + - courses.edit_course_content + - courses.edit_details + - courses.edit_files + - courses.edit_grading_settings + - courses.edit_schedule + - courses.export_course + - courses.export_tags + - courses.import_course + - courses.manage_advanced_settings + - courses.manage_certificates + - courses.manage_course_team + - courses.manage_course_updates + - courses.manage_group_configurations + - courses.manage_library_updates + - courses.manage_pages_and_resources + - courses.manage_tags + - courses.manage_taxonomies + - courses.publish_course_content + - courses.view_advanced_settings + - courses.view_certificates + - courses.view_checklists + - courses.view_course + - courses.view_course_team + - courses.view_course_updates + - courses.view_files + - courses.view_grading_settings + - courses.view_group_configurations + - courses.view_library_updates + - courses.view_pages_and_resources + - courses.view_schedule_and_details + course_team_roles: + - courses.legacy_instructor_role_permissions + course_auditor: + display_name: Course Auditor + description: Can view course content and settings, but cannot make changes. + scopes: + - course-v1 + icon: null + hidden: true + permissions: + course_authoring: + - courses.view_advanced_settings + - courses.view_certificates + - courses.view_checklists + - courses.view_course + - courses.view_course_team + - courses.view_course_updates + - courses.view_files + - courses.view_grading_settings + - courses.view_group_configurations + - courses.view_library_updates + - courses.view_pages_and_resources + - courses.view_schedule_and_details + course_beta_tester: + display_name: Course Beta Tester (legacy) + description: Legacy course role retained for backward compatibility. Grants the legacy beta tester compatibility permission. + scopes: + - course-v1 + icon: null + hidden: true + permissions: + course_team_roles: + - courses.legacy_beta_tester_permissions + course_data_researcher: + display_name: Course Data Researcher (legacy) + description: Legacy course role retained for backward compatibility. Grants the legacy data researcher compatibility permission. + scopes: + - course-v1 + icon: null + hidden: true + permissions: + course_team_roles: + - courses.legacy_data_researcher_permissions + course_editor: + display_name: Course Editor + description: Can create and edit course content, but cannot publish or change critical course settings. + scopes: + - course-v1 + icon: null + hidden: true + permissions: + course_authoring: + - courses.create_files + - courses.edit_course_content + - courses.edit_details + - courses.edit_files + - courses.edit_grading_settings + - courses.manage_course_updates + - courses.manage_group_configurations + - courses.manage_library_updates + - courses.manage_pages_and_resources + - courses.manage_tags + - courses.view_advanced_settings + - courses.view_certificates + - courses.view_checklists + - courses.view_course + - courses.view_course_team + - courses.view_course_updates + - courses.view_files + - courses.view_grading_settings + - courses.view_group_configurations + - courses.view_library_updates + - courses.view_pages_and_resources + - courses.view_schedule_and_details + course_limited_staff: + display_name: Course Limited Staff (legacy) + description: Legacy course role retained for backward compatibility. Grants the legacy limited staff compatibility permission. + scopes: + - course-v1 + icon: null + hidden: true + permissions: + course_team_roles: + - courses.legacy_limited_staff_role_permissions + course_staff: + display_name: Course Staff + description: Can publish content and manage the course lifecycle in Studio. + scopes: + - course-v1 + icon: null + hidden: false + permissions: + course_authoring: + - courses.create_files + - courses.delete_files + - courses.edit_course_content + - courses.edit_details + - courses.edit_files + - courses.edit_grading_settings + - courses.edit_schedule + - courses.export_course + - courses.export_tags + - courses.import_course + - courses.manage_advanced_settings + - courses.manage_certificates + - courses.manage_course_updates + - courses.manage_group_configurations + - courses.manage_library_updates + - courses.manage_pages_and_resources + - courses.manage_tags + - courses.publish_course_content + - courses.view_advanced_settings + - courses.view_certificates + - courses.view_checklists + - courses.view_course + - courses.view_course_team + - courses.view_course_updates + - courses.view_files + - courses.view_grading_settings + - courses.view_group_configurations + - courses.view_library_updates + - courses.view_pages_and_resources + - courses.view_schedule_and_details + course_team_roles: + - courses.legacy_staff_role_permissions + library_admin: + display_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. + scopes: + - lib + icon: null + hidden: false + permissions: + content_libraries: + - content_libraries.create_library_collection + - content_libraries.delete_library + - content_libraries.delete_library_collection + - content_libraries.edit_library_collection + - content_libraries.edit_library_content + - content_libraries.manage_library_tags + - content_libraries.manage_library_team + - content_libraries.publish_library_content + - content_libraries.reuse_library_content + - content_libraries.view_library + - content_libraries.view_library_team + library_author: + display_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. + scopes: + - lib + icon: null + hidden: false + permissions: + content_libraries: + - content_libraries.create_library_collection + - content_libraries.delete_library_collection + - content_libraries.edit_library_collection + - content_libraries.edit_library_content + - content_libraries.manage_library_tags + - content_libraries.publish_library_content + - content_libraries.reuse_library_content + - content_libraries.view_library + - content_libraries.view_library_team + library_contributor: + display_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. + scopes: + - lib + icon: null + hidden: false + permissions: + content_libraries: + - content_libraries.create_library_collection + - content_libraries.delete_library_collection + - content_libraries.edit_library_collection + - content_libraries.edit_library_content + - content_libraries.manage_library_tags + - content_libraries.reuse_library_content + - content_libraries.view_library + - content_libraries.view_library_team + library_user: + display_name: Library User + description: The Library User can view and reuse content but cannot edit or delete any resource. + scopes: + - lib + icon: null + hidden: false + permissions: + content_libraries: + - content_libraries.reuse_library_content + - content_libraries.view_library + - content_libraries.view_library_team +permissions: + content_libraries.create_library_collection: + applet: content_libraries + namespace: content_libraries + name: create_library_collection + display_name: Create + description: Create new collections to organize content within the library. + category_id: library_collection + scopes: + - lib + icon: Plus + content_libraries.delete_library: + applet: content_libraries + namespace: content_libraries + name: delete_library + display_name: Delete + description: Allows users to delete the entire content library. + category_id: library + scopes: + - lib + icon: Delete + content_libraries.delete_library_collection: + applet: content_libraries + namespace: content_libraries + name: delete_library_collection + display_name: Delete + description: Permanently remove collections from the library. + category_id: library_collection + scopes: + - lib + icon: Delete + content_libraries.edit_library_collection: + applet: content_libraries + namespace: content_libraries + name: edit_library_collection + display_name: Edit + description: Update the name and contents of existing collections. + category_id: library_collection + scopes: + - lib + icon: EditOutline + content_libraries.edit_library_content: + applet: content_libraries + namespace: content_libraries + name: edit_library_content + display_name: Edit + description: Create, edit, and delete content items in the library. + category_id: library_content + scopes: + - lib + icon: EditOutline + content_libraries.manage_library_tags: + applet: content_libraries + namespace: content_libraries + name: manage_library_tags + display_name: Manage tags + description: Create, edit, and delete tags on this library. + category_id: library + scopes: + - lib + icon: Settings + content_libraries.manage_library_team: + applet: content_libraries + namespace: content_libraries + name: manage_library_team + display_name: Manage + description: Add, change, or remove role assignments for this library from the Roles and Permissions console. + category_id: library_team + scopes: + - lib + icon: Settings + content_libraries.publish_library_content: + applet: content_libraries + namespace: content_libraries + name: publish_library_content + display_name: Publish + description: Publish individual content items to make them available for reuse in courses. + category_id: library_content + scopes: + - lib + icon: DownloadDone + content_libraries.reuse_library_content: + applet: content_libraries + namespace: content_libraries + name: reuse_library_content + display_name: Reuse + description: Add published content from this library to a course. + category_id: library_content + scopes: + - lib + icon: SpinnerIcon + content_libraries.view_library: + applet: content_libraries + namespace: content_libraries + name: view_library + display_name: View + description: See the library in Studio and access its content in read-only mode. + category_id: library + scopes: + - lib + icon: RemoveRedEye + content_libraries.view_library_team: + applet: content_libraries + namespace: content_libraries + name: view_library_team + display_name: View + description: See the list of users with a role assigned to this library. + category_id: library_team + scopes: + - lib + icon: RemoveRedEye + courses.create_files: + applet: course_authoring + namespace: courses + name: create_files + display_name: Create files + description: Upload new files and assets to the course. + category_id: course_files + scopes: + - course-v1 + icon: Plus + courses.delete_files: + applet: course_authoring + namespace: courses + name: delete_files + display_name: Delete files + description: Permanently remove files and assets from the course. + category_id: course_files + scopes: + - course-v1 + icon: Delete + courses.edit_course_content: + applet: course_authoring + namespace: courses + name: edit_course_content + display_name: Edit course content + description: Edit the course outline, units, and components. + category_id: course_access_content + scopes: + - course-v1 + icon: EditOutline + courses.edit_details: + applet: course_authoring + namespace: courses + name: edit_details + display_name: Edit course details + description: Update course information including the course summary, pacing, and prerequisites. + category_id: course_schedule_details + scopes: + - course-v1 + icon: EditOutline + courses.edit_files: + applet: course_authoring + namespace: courses + name: edit_files + display_name: Edit files + description: Perform non-destructive actions on files, such as locking or unlocking them. + category_id: course_files + scopes: + - course-v1 + icon: EditOutline + courses.edit_grading_settings: + applet: course_authoring + namespace: courses + name: edit_grading_settings + display_name: Edit grading settings + description: Update the grading configuration for the course, including assignment types and grading scale. + category_id: course_grading + scopes: + - course-v1 + icon: EditOutline + courses.edit_schedule: + applet: course_authoring + namespace: courses + name: edit_schedule + display_name: Edit schedule + description: Update course start and end dates, enrollment dates, and pacing settings. + category_id: course_schedule_details + scopes: + - course-v1 + icon: EditOutline + courses.export_course: + applet: course_authoring + namespace: courses + name: export_course + display_name: Export course + description: Download the course content as a file for backup or reuse in another platform. + category_id: course_import_export + scopes: + - course-v1 + icon: Upload + courses.export_tags: + applet: course_authoring + namespace: courses + name: export_tags + display_name: Export tags + description: Download the tag data associated with this course. + category_id: course_import_export + scopes: + - course-v1 + icon: Upload + courses.import_course: + applet: course_authoring + namespace: courses + name: import_course + display_name: Import course + description: Import course content from a file. This is a high-privilege action that can overwrite most course content and settings. + category_id: course_import_export + scopes: + - course-v1 + icon: Download + courses.legacy_beta_tester_permissions: + applet: course_team_roles + namespace: courses + name: legacy_beta_tester_permissions + display_name: Legacy beta tester permissions + description: Compatibility action mapping the legacy course beta tester role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + courses.legacy_data_researcher_permissions: + applet: course_team_roles + namespace: courses + name: legacy_data_researcher_permissions + display_name: Legacy data researcher permissions + description: Compatibility action mapping the legacy course data researcher role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + courses.legacy_instructor_role_permissions: + applet: course_team_roles + namespace: courses + name: legacy_instructor_role_permissions + display_name: Legacy instructor permissions + description: Compatibility action mapping the legacy course instructor role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + courses.legacy_limited_staff_role_permissions: + applet: course_team_roles + namespace: courses + name: legacy_limited_staff_role_permissions + display_name: Legacy limited staff permissions + description: Compatibility action mapping the legacy course limited staff role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + courses.legacy_staff_role_permissions: + applet: course_team_roles + namespace: courses + name: legacy_staff_role_permissions + display_name: Legacy staff permissions + description: Compatibility action mapping the legacy course staff role into the authorization system. + category_id: course_legacy + scopes: + - course-v1 + icon: DrawShapes + courses.manage_advanced_settings: + applet: course_authoring + namespace: courses + name: manage_advanced_settings + display_name: Manage advanced settings + description: Edit technical course configurations in the Advanced Settings page in Studio. + category_id: course_advanced_certificates + scopes: + - course-v1 + icon: Settings + courses.manage_certificates: + applet: course_authoring + namespace: courses + name: manage_certificates + display_name: Manage certificates + description: Create and edit course certificates, including certificate design and eligibility settings. + category_id: course_advanced_certificates + scopes: + - course-v1 + icon: Settings + courses.manage_course_team: + applet: course_authoring + namespace: courses + name: manage_course_team + display_name: Manage course team + description: Add, change, or remove role assignments for this course from the Roles and Permissions console. + category_id: course_team_group + scopes: + - course-v1 + icon: Settings + courses.manage_course_updates: + applet: course_authoring + namespace: courses + name: manage_course_updates + display_name: Manage course updates + description: Create, edit, and delete course announcements and handouts. + category_id: course_updates_handouts + scopes: + - course-v1 + icon: Settings + courses.manage_group_configurations: + applet: course_authoring + namespace: courses + name: manage_group_configurations + display_name: Manage group configurations + description: Create and manage content groups used to target course content to specific learners. + category_id: course_team_group + scopes: + - course-v1 + icon: Settings + courses.manage_library_updates: + applet: course_authoring + namespace: courses + name: manage_library_updates + display_name: Manage library updates + description: Accept or reject pending updates from content libraries linked to this course. + category_id: course_library_updates + scopes: + - course-v1 + icon: Checklist + courses.manage_pages_and_resources: + applet: course_authoring + namespace: courses + name: manage_pages_and_resources + display_name: Manage pages & resources + description: Enable or disable course features such as Discussions, the Wiki, Notes, Calculator, and Live. Create and edit Textbooks and Custom pages, and manage their configurations. + category_id: course_pages_resources + scopes: + - course-v1 + icon: Settings + courses.manage_tags: + applet: course_authoring + namespace: courses + name: manage_tags + display_name: Manage tags + description: Create, edit, and delete tags on this course. + category_id: course_tags_taxonomies + scopes: + - course-v1 + icon: Settings + courses.manage_taxonomies: + applet: course_authoring + namespace: courses + name: manage_taxonomies + display_name: Manage taxonomies + description: Manage taxonomies associated with this course. (Granted to course_admin; not currently surfaced in the admin console.) + category_id: course_tags_taxonomies + scopes: + - course-v1 + icon: LocalOffer + courses.publish_course_content: + applet: course_authoring + namespace: courses + name: publish_course_content + display_name: Publish course content + description: Make course content visible to learners. + category_id: course_access_content + scopes: + - course-v1 + icon: DownloadDone + courses.view_advanced_settings: + applet: course_authoring + namespace: courses + name: view_advanced_settings + display_name: View advanced settings + description: Access the Advanced Settings page in Studio. This covers a wide range of technical course configurations, including proctoring, timed exams, LTI tools, enrollment limits, and custom display options. + category_id: course_advanced_certificates + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_certificates: + applet: course_authoring + namespace: courses + name: view_certificates + display_name: View certificates + description: See the course certificate settings. + category_id: course_advanced_certificates + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_checklists: + applet: course_authoring + namespace: courses + name: view_checklists + display_name: View checklists + description: See the course launch checklist in Studio. + category_id: course_other + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_course: + applet: course_authoring + namespace: courses + name: view_course + display_name: View course + description: See the course in the Studio home and access the course outline in read-only mode. Includes the "View Live" option to preview the course as a learner in the LMS. + category_id: course_access_content + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_course_team: + applet: course_authoring + namespace: courses + name: view_course_team + display_name: View course team + description: See the list of users with a role assigned to this course. + category_id: course_team_group + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_course_updates: + applet: course_authoring + namespace: courses + name: view_course_updates + display_name: View course updates + description: See course announcements and handouts visible to learners. + category_id: course_updates_handouts + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_files: + applet: course_authoring + namespace: courses + name: view_files + display_name: View files + description: See the list of files and assets uploaded to the course. + category_id: course_files + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_grading_settings: + applet: course_authoring + namespace: courses + name: view_grading_settings + display_name: View grading settings + description: See the grading configuration for the course, including assignment types and grading scale. + category_id: course_grading + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_group_configurations: + applet: course_authoring + namespace: courses + name: view_group_configurations + display_name: View group configurations + description: See the list of content groups and their configurations for this course. + category_id: course_team_group + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_library_updates: + applet: course_authoring + namespace: courses + name: view_library_updates + display_name: View library updates + description: View pending updates from content libraries linked to this course. + category_id: course_library_updates + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_pages_and_resources: + applet: course_authoring + namespace: courses + name: view_pages_and_resources + display_name: View pages & resources + description: See the Pages & Resources section in Studio. + category_id: course_pages_resources + scopes: + - course-v1 + icon: RemoveRedEye + courses.view_schedule_and_details: + applet: course_authoring + namespace: courses + name: view_schedule_and_details + display_name: View schedule & details + description: See the course schedule (start and end dates, enrollment dates, and pacing settings) and course details (summary, pacing, and prerequisites). + category_id: course_schedule_details + scopes: + - course-v1 + icon: RemoveRedEye +categories: + course_access_content: + applet: course_authoring + display_name: Course access & content + description: Permissions related to accessing the course and managing core course content, including creating, editing, and publishing materials. + icon: BookOpen + course_advanced_certificates: + applet: course_authoring + display_name: Advanced & certificates + description: Permissions for managing advanced course settings and course certificates. + icon: CheckCircle + course_files: + applet: course_authoring + display_name: Files + description: Permissions for viewing and managing files and assets uploaded to the course. + icon: Folder + course_grading: + applet: course_authoring + display_name: Grading + description: Permissions related to viewing and managing grading configuration and grading policies. + icon: Award + course_import_export: + applet: course_authoring + display_name: Import / export + description: Permissions for importing and exporting course content and related data. + icon: Download + course_legacy: + applet: course_team_roles + display_name: Legacy compatibility + description: Internal compatibility permissions that map legacy course roles to the authorization system. Not shown as individually assignable permissions. + icon: DrawShapes + course_library_updates: + applet: course_authoring + display_name: Library updates + description: Permissions for reviewing and managing updates made to content libraries connected to the course. + icon: LibraryBooks + course_other: + applet: course_authoring + display_name: Other + description: Additional permissions not included in other categories, such as viewing checklists. + icon: DrawShapes + course_pages_resources: + applet: course_authoring + display_name: Pages & resources + description: Permissions for viewing and managing course pages and additional learning resources. + icon: Article + course_schedule_details: + applet: course_authoring + display_name: Schedule & details + description: Permissions for viewing and editing the course schedule and course information. + icon: Calendar + course_tags_taxonomies: + applet: course_authoring + display_name: Tags + description: Permissions for managing tags used to organize course content. + icon: LocalOffer + course_team_group: + applet: course_authoring + display_name: Course team & groups + description: Permissions for viewing and managing the course team, learner groups, and group configurations. + icon: Group + course_updates_handouts: + applet: course_authoring + display_name: Course updates & handouts + description: Permissions for viewing and managing course updates and handouts that are visible to learners. + icon: Sync + library: + applet: content_libraries + display_name: Library + description: Permissions related to viewing, managing, and publishing the library structure and metadata. + icon: CollectionsBookmark + library_collection: + applet: content_libraries + display_name: Collection + description: Permissions for creating and managing content collections within the library. + icon: AutoAwesomeMosaic + library_content: + applet: content_libraries + display_name: Content + description: Permissions for editing, publishing, and reusing content within the library. + icon: Notes + library_team: + applet: content_libraries + display_name: Team + description: Permissions for viewing and managing users who have access to the library. + icon: Group diff --git a/docs/references/index.rst b/docs/references/index.rst index 204ae634..15b51552 100644 --- a/docs/references/index.rst +++ b/docs/references/index.rst @@ -5,3 +5,4 @@ References :maxdepth: 1 authorization-schema + compiled-schema diff --git a/scripts/generate_compiled_schema.py b/scripts/generate_compiled_schema.py new file mode 100644 index 00000000..a5d29513 --- /dev/null +++ b/scripts/generate_compiled_schema.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +"""Regenerate the compiled schema snapshot in ``docs/references/compiled-schema.yaml``. + +Usage:: + + make compiled_schema +""" + +import os + +import django +import yaml + +HEADER = """\ +# Compiled authorization schema of the built-in applets, generated by `make compiled_schema`. +# Each role lists its permissions grouped by the applet that adds them. +""" + + +def main(): + """Compile the built-in schema and write it to the snapshot file.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openedx_authz.settings.test") + django.setup() + + # These modules read Django settings on import, so they load after setup. + from openedx_authz.engine.schema.pipeline import SchemaPipeline + from openedx_authz.tests.applets.test_builtin_definitions import COMPILED_SCHEMA_SNAPSHOT, snapshot_data + + data = snapshot_data(SchemaPipeline().compile()) + content = yaml.safe_dump(data, sort_keys=False, allow_unicode=True, width=1000) + COMPILED_SCHEMA_SNAPSHOT.write_text(HEADER + content, encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/src/openedx_authz/tests/applets/fixtures/compiled_schema.json b/src/openedx_authz/tests/applets/fixtures/compiled_schema.json deleted file mode 100644 index f3288358..00000000 --- a/src/openedx_authz/tests/applets/fixtures/compiled_schema.json +++ /dev/null @@ -1,1432 +0,0 @@ -{ - "categories": { - "course_access_content": { - "description": "Permissions related to accessing the course and managing core course content, including creating, editing, and publishing materials.", - "display_name": "Course access & content", - "icon": "BookOpen", - "id": "course_access_content" - }, - "course_advanced_certificates": { - "description": "Permissions for managing advanced course settings and course certificates.", - "display_name": "Advanced & certificates", - "icon": "CheckCircle", - "id": "course_advanced_certificates" - }, - "course_files": { - "description": "Permissions for viewing and managing files and assets uploaded to the course.", - "display_name": "Files", - "icon": "Folder", - "id": "course_files" - }, - "course_grading": { - "description": "Permissions related to viewing and managing grading configuration and grading policies.", - "display_name": "Grading", - "icon": "Award", - "id": "course_grading" - }, - "course_import_export": { - "description": "Permissions for importing and exporting course content and related data.", - "display_name": "Import / export", - "icon": "Download", - "id": "course_import_export" - }, - "course_legacy": { - "description": "Internal compatibility permissions that map legacy course roles to the authorization system. Not shown as individually assignable permissions.", - "display_name": "Legacy compatibility", - "icon": "DrawShapes", - "id": "course_legacy" - }, - "course_library_updates": { - "description": "Permissions for reviewing and managing updates made to content libraries connected to the course.", - "display_name": "Library updates", - "icon": "LibraryBooks", - "id": "course_library_updates" - }, - "course_other": { - "description": "Additional permissions not included in other categories, such as viewing checklists.", - "display_name": "Other", - "icon": "DrawShapes", - "id": "course_other" - }, - "course_pages_resources": { - "description": "Permissions for viewing and managing course pages and additional learning resources.", - "display_name": "Pages & resources", - "icon": "Article", - "id": "course_pages_resources" - }, - "course_schedule_details": { - "description": "Permissions for viewing and editing the course schedule and course information.", - "display_name": "Schedule & details", - "icon": "Calendar", - "id": "course_schedule_details" - }, - "course_tags_taxonomies": { - "description": "Permissions for managing tags used to organize course content.", - "display_name": "Tags", - "icon": "LocalOffer", - "id": "course_tags_taxonomies" - }, - "course_team_group": { - "description": "Permissions for viewing and managing the course team, learner groups, and group configurations.", - "display_name": "Course team & groups", - "icon": "Group", - "id": "course_team_group" - }, - "course_updates_handouts": { - "description": "Permissions for viewing and managing course updates and handouts that are visible to learners.", - "display_name": "Course updates & handouts", - "icon": "Sync", - "id": "course_updates_handouts" - }, - "library": { - "description": "Permissions related to viewing, managing, and publishing the library structure and metadata.", - "display_name": "Library", - "icon": "CollectionsBookmark", - "id": "library" - }, - "library_collection": { - "description": "Permissions for creating and managing content collections within the library.", - "display_name": "Collection", - "icon": "AutoAwesomeMosaic", - "id": "library_collection" - }, - "library_content": { - "description": "Permissions for editing, publishing, and reusing content within the library.", - "display_name": "Content", - "icon": "Notes", - "id": "library_content" - }, - "library_team": { - "description": "Permissions for viewing and managing users who have access to the library.", - "display_name": "Team", - "icon": "Group", - "id": "library_team" - } - }, - "permissions": { - "content_libraries.create_library_collection": { - "category_id": "library_collection", - "description": "Create new collections to organize content within the library.", - "display_name": "Create", - "icon": "Plus", - "name": "create_library_collection", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.delete_library": { - "category_id": "library", - "description": "Allows users to delete the entire content library.", - "display_name": "Delete", - "icon": "Delete", - "name": "delete_library", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.delete_library_collection": { - "category_id": "library_collection", - "description": "Permanently remove collections from the library.", - "display_name": "Delete", - "icon": "Delete", - "name": "delete_library_collection", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.edit_library_collection": { - "category_id": "library_collection", - "description": "Update the name and contents of existing collections.", - "display_name": "Edit", - "icon": "EditOutline", - "name": "edit_library_collection", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.edit_library_content": { - "category_id": "library_content", - "description": "Create, edit, and delete content items in the library.", - "display_name": "Edit", - "icon": "EditOutline", - "name": "edit_library_content", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.manage_library_tags": { - "category_id": "library", - "description": "Create, edit, and delete tags on this library.", - "display_name": "Manage tags", - "icon": "Settings", - "name": "manage_library_tags", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.manage_library_team": { - "category_id": "library_team", - "description": "Add, change, or remove role assignments for this library from the Roles and Permissions console.", - "display_name": "Manage", - "icon": "Settings", - "name": "manage_library_team", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.publish_library_content": { - "category_id": "library_content", - "description": "Publish individual content items to make them available for reuse in courses.", - "display_name": "Publish", - "icon": "DownloadDone", - "name": "publish_library_content", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.reuse_library_content": { - "category_id": "library_content", - "description": "Add published content from this library to a course.", - "display_name": "Reuse", - "icon": "SpinnerIcon", - "name": "reuse_library_content", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.view_library": { - "category_id": "library", - "description": "See the library in Studio and access its content in read-only mode.", - "display_name": "View", - "icon": "RemoveRedEye", - "name": "view_library", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "content_libraries.view_library_team": { - "category_id": "library_team", - "description": "See the list of users with a role assigned to this library.", - "display_name": "View", - "icon": "RemoveRedEye", - "name": "view_library_team", - "namespace": "content_libraries", - "scopes": [ - "lib" - ] - }, - "courses.create_files": { - "category_id": "course_files", - "description": "Upload new files and assets to the course.", - "display_name": "Create files", - "icon": "Plus", - "name": "create_files", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.delete_files": { - "category_id": "course_files", - "description": "Permanently remove files and assets from the course.", - "display_name": "Delete files", - "icon": "Delete", - "name": "delete_files", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.edit_course_content": { - "category_id": "course_access_content", - "description": "Edit the course outline, units, and components.", - "display_name": "Edit course content", - "icon": "EditOutline", - "name": "edit_course_content", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.edit_details": { - "category_id": "course_schedule_details", - "description": "Update course information including the course summary, pacing, and prerequisites.", - "display_name": "Edit course details", - "icon": "EditOutline", - "name": "edit_details", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.edit_files": { - "category_id": "course_files", - "description": "Perform non-destructive actions on files, such as locking or unlocking them.", - "display_name": "Edit files", - "icon": "EditOutline", - "name": "edit_files", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.edit_grading_settings": { - "category_id": "course_grading", - "description": "Update the grading configuration for the course, including assignment types and grading scale.", - "display_name": "Edit grading settings", - "icon": "EditOutline", - "name": "edit_grading_settings", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.edit_schedule": { - "category_id": "course_schedule_details", - "description": "Update course start and end dates, enrollment dates, and pacing settings.", - "display_name": "Edit schedule", - "icon": "EditOutline", - "name": "edit_schedule", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.export_course": { - "category_id": "course_import_export", - "description": "Download the course content as a file for backup or reuse in another platform.", - "display_name": "Export course", - "icon": "Upload", - "name": "export_course", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.export_tags": { - "category_id": "course_import_export", - "description": "Download the tag data associated with this course.", - "display_name": "Export tags", - "icon": "Upload", - "name": "export_tags", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.import_course": { - "category_id": "course_import_export", - "description": "Import course content from a file. This is a high-privilege action that can overwrite most course content and settings.", - "display_name": "Import course", - "icon": "Download", - "name": "import_course", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.legacy_beta_tester_permissions": { - "category_id": "course_legacy", - "description": "Compatibility action mapping the legacy course beta tester role into the authorization system.", - "display_name": "Legacy beta tester permissions", - "icon": "DrawShapes", - "name": "legacy_beta_tester_permissions", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.legacy_data_researcher_permissions": { - "category_id": "course_legacy", - "description": "Compatibility action mapping the legacy course data researcher role into the authorization system.", - "display_name": "Legacy data researcher permissions", - "icon": "DrawShapes", - "name": "legacy_data_researcher_permissions", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.legacy_instructor_role_permissions": { - "category_id": "course_legacy", - "description": "Compatibility action mapping the legacy course instructor role into the authorization system.", - "display_name": "Legacy instructor permissions", - "icon": "DrawShapes", - "name": "legacy_instructor_role_permissions", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.legacy_limited_staff_role_permissions": { - "category_id": "course_legacy", - "description": "Compatibility action mapping the legacy course limited staff role into the authorization system.", - "display_name": "Legacy limited staff permissions", - "icon": "DrawShapes", - "name": "legacy_limited_staff_role_permissions", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.legacy_staff_role_permissions": { - "category_id": "course_legacy", - "description": "Compatibility action mapping the legacy course staff role into the authorization system.", - "display_name": "Legacy staff permissions", - "icon": "DrawShapes", - "name": "legacy_staff_role_permissions", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_advanced_settings": { - "category_id": "course_advanced_certificates", - "description": "Edit technical course configurations in the Advanced Settings page in Studio.", - "display_name": "Manage advanced settings", - "icon": "Settings", - "name": "manage_advanced_settings", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_certificates": { - "category_id": "course_advanced_certificates", - "description": "Create and edit course certificates, including certificate design and eligibility settings.", - "display_name": "Manage certificates", - "icon": "Settings", - "name": "manage_certificates", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_course_team": { - "category_id": "course_team_group", - "description": "Add, change, or remove role assignments for this course from the Roles and Permissions console.", - "display_name": "Manage course team", - "icon": "Settings", - "name": "manage_course_team", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_course_updates": { - "category_id": "course_updates_handouts", - "description": "Create, edit, and delete course announcements and handouts.", - "display_name": "Manage course updates", - "icon": "Settings", - "name": "manage_course_updates", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_group_configurations": { - "category_id": "course_team_group", - "description": "Create and manage content groups used to target course content to specific learners.", - "display_name": "Manage group configurations", - "icon": "Settings", - "name": "manage_group_configurations", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_library_updates": { - "category_id": "course_library_updates", - "description": "Accept or reject pending updates from content libraries linked to this course.", - "display_name": "Manage library updates", - "icon": "Checklist", - "name": "manage_library_updates", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_pages_and_resources": { - "category_id": "course_pages_resources", - "description": "Enable or disable course features such as Discussions, the Wiki, Notes, Calculator, and Live. Create and edit Textbooks and Custom pages, and manage their configurations.", - "display_name": "Manage pages & resources", - "icon": "Settings", - "name": "manage_pages_and_resources", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_tags": { - "category_id": "course_tags_taxonomies", - "description": "Create, edit, and delete tags on this course.", - "display_name": "Manage tags", - "icon": "Settings", - "name": "manage_tags", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.manage_taxonomies": { - "category_id": "course_tags_taxonomies", - "description": "Manage taxonomies associated with this course. (Granted to course_admin; not currently surfaced in the admin console.)", - "display_name": "Manage taxonomies", - "icon": "LocalOffer", - "name": "manage_taxonomies", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.publish_course_content": { - "category_id": "course_access_content", - "description": "Make course content visible to learners.", - "display_name": "Publish course content", - "icon": "DownloadDone", - "name": "publish_course_content", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_advanced_settings": { - "category_id": "course_advanced_certificates", - "description": "Access the Advanced Settings page in Studio. This covers a wide range of technical course configurations, including proctoring, timed exams, LTI tools, enrollment limits, and custom display options.", - "display_name": "View advanced settings", - "icon": "RemoveRedEye", - "name": "view_advanced_settings", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_certificates": { - "category_id": "course_advanced_certificates", - "description": "See the course certificate settings.", - "display_name": "View certificates", - "icon": "RemoveRedEye", - "name": "view_certificates", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_checklists": { - "category_id": "course_other", - "description": "See the course launch checklist in Studio.", - "display_name": "View checklists", - "icon": "RemoveRedEye", - "name": "view_checklists", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_course": { - "category_id": "course_access_content", - "description": "See the course in the Studio home and access the course outline in read-only mode. Includes the \"View Live\" option to preview the course as a learner in the LMS.", - "display_name": "View course", - "icon": "RemoveRedEye", - "name": "view_course", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_course_team": { - "category_id": "course_team_group", - "description": "See the list of users with a role assigned to this course.", - "display_name": "View course team", - "icon": "RemoveRedEye", - "name": "view_course_team", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_course_updates": { - "category_id": "course_updates_handouts", - "description": "See course announcements and handouts visible to learners.", - "display_name": "View course updates", - "icon": "RemoveRedEye", - "name": "view_course_updates", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_files": { - "category_id": "course_files", - "description": "See the list of files and assets uploaded to the course.", - "display_name": "View files", - "icon": "RemoveRedEye", - "name": "view_files", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_grading_settings": { - "category_id": "course_grading", - "description": "See the grading configuration for the course, including assignment types and grading scale.", - "display_name": "View grading settings", - "icon": "RemoveRedEye", - "name": "view_grading_settings", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_group_configurations": { - "category_id": "course_team_group", - "description": "See the list of content groups and their configurations for this course.", - "display_name": "View group configurations", - "icon": "RemoveRedEye", - "name": "view_group_configurations", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_library_updates": { - "category_id": "course_library_updates", - "description": "View pending updates from content libraries linked to this course.", - "display_name": "View library updates", - "icon": "RemoveRedEye", - "name": "view_library_updates", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_pages_and_resources": { - "category_id": "course_pages_resources", - "description": "See the Pages & Resources section in Studio.", - "display_name": "View pages & resources", - "icon": "RemoveRedEye", - "name": "view_pages_and_resources", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - }, - "courses.view_schedule_and_details": { - "category_id": "course_schedule_details", - "description": "See the course schedule (start and end dates, enrollment dates, and pacing settings) and course details (summary, pacing, and prerequisites).", - "display_name": "View schedule & details", - "icon": "RemoveRedEye", - "name": "view_schedule_and_details", - "namespace": "courses", - "scopes": [ - "course-v1" - ] - } - }, - "role_permission_pairs": [ - [ - "course_admin", - "courses.create_files" - ], - [ - "course_admin", - "courses.delete_files" - ], - [ - "course_admin", - "courses.edit_course_content" - ], - [ - "course_admin", - "courses.edit_details" - ], - [ - "course_admin", - "courses.edit_files" - ], - [ - "course_admin", - "courses.edit_grading_settings" - ], - [ - "course_admin", - "courses.edit_schedule" - ], - [ - "course_admin", - "courses.export_course" - ], - [ - "course_admin", - "courses.export_tags" - ], - [ - "course_admin", - "courses.import_course" - ], - [ - "course_admin", - "courses.legacy_instructor_role_permissions" - ], - [ - "course_admin", - "courses.manage_advanced_settings" - ], - [ - "course_admin", - "courses.manage_certificates" - ], - [ - "course_admin", - "courses.manage_course_team" - ], - [ - "course_admin", - "courses.manage_course_updates" - ], - [ - "course_admin", - "courses.manage_group_configurations" - ], - [ - "course_admin", - "courses.manage_library_updates" - ], - [ - "course_admin", - "courses.manage_pages_and_resources" - ], - [ - "course_admin", - "courses.manage_tags" - ], - [ - "course_admin", - "courses.manage_taxonomies" - ], - [ - "course_admin", - "courses.publish_course_content" - ], - [ - "course_admin", - "courses.view_advanced_settings" - ], - [ - "course_admin", - "courses.view_certificates" - ], - [ - "course_admin", - "courses.view_checklists" - ], - [ - "course_admin", - "courses.view_course" - ], - [ - "course_admin", - "courses.view_course_team" - ], - [ - "course_admin", - "courses.view_course_updates" - ], - [ - "course_admin", - "courses.view_files" - ], - [ - "course_admin", - "courses.view_grading_settings" - ], - [ - "course_admin", - "courses.view_group_configurations" - ], - [ - "course_admin", - "courses.view_library_updates" - ], - [ - "course_admin", - "courses.view_pages_and_resources" - ], - [ - "course_admin", - "courses.view_schedule_and_details" - ], - [ - "course_auditor", - "courses.view_advanced_settings" - ], - [ - "course_auditor", - "courses.view_certificates" - ], - [ - "course_auditor", - "courses.view_checklists" - ], - [ - "course_auditor", - "courses.view_course" - ], - [ - "course_auditor", - "courses.view_course_team" - ], - [ - "course_auditor", - "courses.view_course_updates" - ], - [ - "course_auditor", - "courses.view_files" - ], - [ - "course_auditor", - "courses.view_grading_settings" - ], - [ - "course_auditor", - "courses.view_group_configurations" - ], - [ - "course_auditor", - "courses.view_library_updates" - ], - [ - "course_auditor", - "courses.view_pages_and_resources" - ], - [ - "course_auditor", - "courses.view_schedule_and_details" - ], - [ - "course_beta_tester", - "courses.legacy_beta_tester_permissions" - ], - [ - "course_data_researcher", - "courses.legacy_data_researcher_permissions" - ], - [ - "course_editor", - "courses.create_files" - ], - [ - "course_editor", - "courses.edit_course_content" - ], - [ - "course_editor", - "courses.edit_details" - ], - [ - "course_editor", - "courses.edit_files" - ], - [ - "course_editor", - "courses.edit_grading_settings" - ], - [ - "course_editor", - "courses.manage_course_updates" - ], - [ - "course_editor", - "courses.manage_group_configurations" - ], - [ - "course_editor", - "courses.manage_library_updates" - ], - [ - "course_editor", - "courses.manage_pages_and_resources" - ], - [ - "course_editor", - "courses.manage_tags" - ], - [ - "course_editor", - "courses.view_advanced_settings" - ], - [ - "course_editor", - "courses.view_certificates" - ], - [ - "course_editor", - "courses.view_checklists" - ], - [ - "course_editor", - "courses.view_course" - ], - [ - "course_editor", - "courses.view_course_team" - ], - [ - "course_editor", - "courses.view_course_updates" - ], - [ - "course_editor", - "courses.view_files" - ], - [ - "course_editor", - "courses.view_grading_settings" - ], - [ - "course_editor", - "courses.view_group_configurations" - ], - [ - "course_editor", - "courses.view_library_updates" - ], - [ - "course_editor", - "courses.view_pages_and_resources" - ], - [ - "course_editor", - "courses.view_schedule_and_details" - ], - [ - "course_limited_staff", - "courses.legacy_limited_staff_role_permissions" - ], - [ - "course_staff", - "courses.create_files" - ], - [ - "course_staff", - "courses.delete_files" - ], - [ - "course_staff", - "courses.edit_course_content" - ], - [ - "course_staff", - "courses.edit_details" - ], - [ - "course_staff", - "courses.edit_files" - ], - [ - "course_staff", - "courses.edit_grading_settings" - ], - [ - "course_staff", - "courses.edit_schedule" - ], - [ - "course_staff", - "courses.export_course" - ], - [ - "course_staff", - "courses.export_tags" - ], - [ - "course_staff", - "courses.import_course" - ], - [ - "course_staff", - "courses.legacy_staff_role_permissions" - ], - [ - "course_staff", - "courses.manage_advanced_settings" - ], - [ - "course_staff", - "courses.manage_certificates" - ], - [ - "course_staff", - "courses.manage_course_updates" - ], - [ - "course_staff", - "courses.manage_group_configurations" - ], - [ - "course_staff", - "courses.manage_library_updates" - ], - [ - "course_staff", - "courses.manage_pages_and_resources" - ], - [ - "course_staff", - "courses.manage_tags" - ], - [ - "course_staff", - "courses.publish_course_content" - ], - [ - "course_staff", - "courses.view_advanced_settings" - ], - [ - "course_staff", - "courses.view_certificates" - ], - [ - "course_staff", - "courses.view_checklists" - ], - [ - "course_staff", - "courses.view_course" - ], - [ - "course_staff", - "courses.view_course_team" - ], - [ - "course_staff", - "courses.view_course_updates" - ], - [ - "course_staff", - "courses.view_files" - ], - [ - "course_staff", - "courses.view_grading_settings" - ], - [ - "course_staff", - "courses.view_group_configurations" - ], - [ - "course_staff", - "courses.view_library_updates" - ], - [ - "course_staff", - "courses.view_pages_and_resources" - ], - [ - "course_staff", - "courses.view_schedule_and_details" - ], - [ - "library_admin", - "content_libraries.create_library_collection" - ], - [ - "library_admin", - "content_libraries.delete_library" - ], - [ - "library_admin", - "content_libraries.delete_library_collection" - ], - [ - "library_admin", - "content_libraries.edit_library_collection" - ], - [ - "library_admin", - "content_libraries.edit_library_content" - ], - [ - "library_admin", - "content_libraries.manage_library_tags" - ], - [ - "library_admin", - "content_libraries.manage_library_team" - ], - [ - "library_admin", - "content_libraries.publish_library_content" - ], - [ - "library_admin", - "content_libraries.reuse_library_content" - ], - [ - "library_admin", - "content_libraries.view_library" - ], - [ - "library_admin", - "content_libraries.view_library_team" - ], - [ - "library_author", - "content_libraries.create_library_collection" - ], - [ - "library_author", - "content_libraries.delete_library_collection" - ], - [ - "library_author", - "content_libraries.edit_library_collection" - ], - [ - "library_author", - "content_libraries.edit_library_content" - ], - [ - "library_author", - "content_libraries.manage_library_tags" - ], - [ - "library_author", - "content_libraries.publish_library_content" - ], - [ - "library_author", - "content_libraries.reuse_library_content" - ], - [ - "library_author", - "content_libraries.view_library" - ], - [ - "library_author", - "content_libraries.view_library_team" - ], - [ - "library_contributor", - "content_libraries.create_library_collection" - ], - [ - "library_contributor", - "content_libraries.delete_library_collection" - ], - [ - "library_contributor", - "content_libraries.edit_library_collection" - ], - [ - "library_contributor", - "content_libraries.edit_library_content" - ], - [ - "library_contributor", - "content_libraries.manage_library_tags" - ], - [ - "library_contributor", - "content_libraries.reuse_library_content" - ], - [ - "library_contributor", - "content_libraries.view_library" - ], - [ - "library_contributor", - "content_libraries.view_library_team" - ], - [ - "library_user", - "content_libraries.reuse_library_content" - ], - [ - "library_user", - "content_libraries.view_library" - ], - [ - "library_user", - "content_libraries.view_library_team" - ] - ], - "roles": { - "course_admin": { - "description": "Can manage the course team and all course settings.", - "display_name": "Course Admin", - "hidden": false, - "icon": null, - "id": "course_admin", - "permissions": [ - "courses.create_files", - "courses.delete_files", - "courses.edit_course_content", - "courses.edit_details", - "courses.edit_files", - "courses.edit_grading_settings", - "courses.edit_schedule", - "courses.export_course", - "courses.export_tags", - "courses.import_course", - "courses.legacy_instructor_role_permissions", - "courses.manage_advanced_settings", - "courses.manage_certificates", - "courses.manage_course_team", - "courses.manage_course_updates", - "courses.manage_group_configurations", - "courses.manage_library_updates", - "courses.manage_pages_and_resources", - "courses.manage_tags", - "courses.manage_taxonomies", - "courses.publish_course_content", - "courses.view_advanced_settings", - "courses.view_certificates", - "courses.view_checklists", - "courses.view_course", - "courses.view_course_team", - "courses.view_course_updates", - "courses.view_files", - "courses.view_grading_settings", - "courses.view_group_configurations", - "courses.view_library_updates", - "courses.view_pages_and_resources", - "courses.view_schedule_and_details" - ], - "scopes": [ - "course-v1" - ] - }, - "course_auditor": { - "description": "Can view course content and settings, but cannot make changes.", - "display_name": "Course Auditor", - "hidden": true, - "icon": null, - "id": "course_auditor", - "permissions": [ - "courses.view_advanced_settings", - "courses.view_certificates", - "courses.view_checklists", - "courses.view_course", - "courses.view_course_team", - "courses.view_course_updates", - "courses.view_files", - "courses.view_grading_settings", - "courses.view_group_configurations", - "courses.view_library_updates", - "courses.view_pages_and_resources", - "courses.view_schedule_and_details" - ], - "scopes": [ - "course-v1" - ] - }, - "course_beta_tester": { - "description": "Legacy course role retained for backward compatibility. Grants the legacy beta tester compatibility permission.", - "display_name": "Course Beta Tester (legacy)", - "hidden": true, - "icon": null, - "id": "course_beta_tester", - "permissions": [ - "courses.legacy_beta_tester_permissions" - ], - "scopes": [ - "course-v1" - ] - }, - "course_data_researcher": { - "description": "Legacy course role retained for backward compatibility. Grants the legacy data researcher compatibility permission.", - "display_name": "Course Data Researcher (legacy)", - "hidden": true, - "icon": null, - "id": "course_data_researcher", - "permissions": [ - "courses.legacy_data_researcher_permissions" - ], - "scopes": [ - "course-v1" - ] - }, - "course_editor": { - "description": "Can create and edit course content, but cannot publish or change critical course settings.", - "display_name": "Course Editor", - "hidden": true, - "icon": null, - "id": "course_editor", - "permissions": [ - "courses.create_files", - "courses.edit_course_content", - "courses.edit_details", - "courses.edit_files", - "courses.edit_grading_settings", - "courses.manage_course_updates", - "courses.manage_group_configurations", - "courses.manage_library_updates", - "courses.manage_pages_and_resources", - "courses.manage_tags", - "courses.view_advanced_settings", - "courses.view_certificates", - "courses.view_checklists", - "courses.view_course", - "courses.view_course_team", - "courses.view_course_updates", - "courses.view_files", - "courses.view_grading_settings", - "courses.view_group_configurations", - "courses.view_library_updates", - "courses.view_pages_and_resources", - "courses.view_schedule_and_details" - ], - "scopes": [ - "course-v1" - ] - }, - "course_limited_staff": { - "description": "Legacy course role retained for backward compatibility. Grants the legacy limited staff compatibility permission.", - "display_name": "Course Limited Staff (legacy)", - "hidden": true, - "icon": null, - "id": "course_limited_staff", - "permissions": [ - "courses.legacy_limited_staff_role_permissions" - ], - "scopes": [ - "course-v1" - ] - }, - "course_staff": { - "description": "Can publish content and manage the course lifecycle in Studio.", - "display_name": "Course Staff", - "hidden": false, - "icon": null, - "id": "course_staff", - "permissions": [ - "courses.create_files", - "courses.delete_files", - "courses.edit_course_content", - "courses.edit_details", - "courses.edit_files", - "courses.edit_grading_settings", - "courses.edit_schedule", - "courses.export_course", - "courses.export_tags", - "courses.import_course", - "courses.legacy_staff_role_permissions", - "courses.manage_advanced_settings", - "courses.manage_certificates", - "courses.manage_course_updates", - "courses.manage_group_configurations", - "courses.manage_library_updates", - "courses.manage_pages_and_resources", - "courses.manage_tags", - "courses.publish_course_content", - "courses.view_advanced_settings", - "courses.view_certificates", - "courses.view_checklists", - "courses.view_course", - "courses.view_course_team", - "courses.view_course_updates", - "courses.view_files", - "courses.view_grading_settings", - "courses.view_group_configurations", - "courses.view_library_updates", - "courses.view_pages_and_resources", - "courses.view_schedule_and_details" - ], - "scopes": [ - "course-v1" - ] - }, - "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.", - "display_name": "Library Admin", - "hidden": false, - "icon": null, - "id": "library_admin", - "permissions": [ - "content_libraries.create_library_collection", - "content_libraries.delete_library", - "content_libraries.delete_library_collection", - "content_libraries.edit_library_collection", - "content_libraries.edit_library_content", - "content_libraries.manage_library_tags", - "content_libraries.manage_library_team", - "content_libraries.publish_library_content", - "content_libraries.reuse_library_content", - "content_libraries.view_library", - "content_libraries.view_library_team" - ], - "scopes": [ - "lib" - ] - }, - "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.", - "display_name": "Library Author", - "hidden": false, - "icon": null, - "id": "library_author", - "permissions": [ - "content_libraries.create_library_collection", - "content_libraries.delete_library_collection", - "content_libraries.edit_library_collection", - "content_libraries.edit_library_content", - "content_libraries.manage_library_tags", - "content_libraries.publish_library_content", - "content_libraries.reuse_library_content", - "content_libraries.view_library", - "content_libraries.view_library_team" - ], - "scopes": [ - "lib" - ] - }, - "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.", - "display_name": "Library Contributor", - "hidden": false, - "icon": null, - "id": "library_contributor", - "permissions": [ - "content_libraries.create_library_collection", - "content_libraries.delete_library_collection", - "content_libraries.edit_library_collection", - "content_libraries.edit_library_content", - "content_libraries.manage_library_tags", - "content_libraries.reuse_library_content", - "content_libraries.view_library", - "content_libraries.view_library_team" - ], - "scopes": [ - "lib" - ] - }, - "library_user": { - "description": "The Library User can view and reuse content but cannot edit or delete any resource.", - "display_name": "Library User", - "hidden": false, - "icon": null, - "id": "library_user", - "permissions": [ - "content_libraries.reuse_library_content", - "content_libraries.view_library", - "content_libraries.view_library_team" - ], - "scopes": [ - "lib" - ] - } - } -} diff --git a/src/openedx_authz/tests/applets/test_builtin_definitions.py b/src/openedx_authz/tests/applets/test_builtin_definitions.py index aebf4716..9a5dac6a 100644 --- a/src/openedx_authz/tests/applets/test_builtin_definitions.py +++ b/src/openedx_authz/tests/applets/test_builtin_definitions.py @@ -1,42 +1,67 @@ """Compatibility tests for definitions contributed by built-in applets.""" -import json from dataclasses import asdict from pathlib import Path +import yaml + from openedx_authz.constants import permissions, roles from openedx_authz.data import PermissionData from openedx_authz.engine.schema.pipeline import SchemaPipeline -COMPILED_SCHEMA_FIXTURE = Path(__file__).with_name("fixtures") / "compiled_schema.json" +COMPILED_SCHEMA_SNAPSHOT = Path(__file__).parents[4] / "docs" / "references" / "compiled-schema.yaml" -def _schema_data(compiled): - """Return the behavior-bearing compiled schema fields in a stable form.""" - role_definitions = {} - for key, value in sorted(compiled.roles.items()): - definition = asdict(value.definition) - definition["permissions"] = sorted(definition["permissions"]) - role_definitions[key] = definition +def applet_name(source): + """Return the applet that owns a schema source, or its module when no applet owns it.""" + return source.module.removeprefix("openedx_authz.applets.").removesuffix(".schema") + - data = { - "categories": {key: asdict(value.definition) for key, value in sorted(compiled.categories.items())}, - "permissions": {key: asdict(value.definition) for key, value in sorted(compiled.permissions.items())}, - "roles": role_definitions, - "role_permission_pairs": compiled.role_permission_pairs(), - } - return json.loads(json.dumps(data)) +def snapshot_data(compiled): + """Return the compiled schema as a readable mapping of roles, permissions, and categories. + + Each role lists its permissions grouped by the applet that adds them, so the snapshot shows + how base definitions and role extensions compose each role. + """ + role_definitions = {} + for role_id, compiled_role in sorted(compiled.roles.items()): + definition = asdict(compiled_role.definition) + del definition["id"] + definition["scopes"] = list(definition["scopes"]) + permissions_by_applet = {} + for permission in sorted(definition.pop("permissions")): + for relationship in compiled.role_permission_sources[(role_id, permission)]: + permissions_by_applet.setdefault(applet_name(relationship.source), []).append(permission) + definition["permissions"] = dict(sorted(permissions_by_applet.items())) + role_definitions[role_id] = definition + + permission_definitions = {} + for permission_id, compiled_permission in sorted(compiled.permissions.items()): + definition = asdict(compiled_permission.definition) + definition["scopes"] = list(definition["scopes"]) + permission_definitions[permission_id] = {"applet": applet_name(compiled_permission.sources[0]), **definition} + + category_definitions = {} + for category_id, compiled_category in sorted(compiled.categories.items()): + definition = asdict(compiled_category.definition) + del definition["id"] + category_definitions[category_id] = {"applet": applet_name(compiled_category.sources[0]), **definition} + + return {"roles": role_definitions, "permissions": permission_definitions, "categories": category_definitions} class TestBuiltinAppletDefinitions: """Built-in applets compose the expected public authorization schema.""" def test_compiled_schema_matches_expected_definitions(self): - """The compiled applet schema matches the checked-in definition set.""" + """The compiled applet schema matches the checked-in snapshot. + + Regenerate the snapshot with ``make compiled_schema`` after changing an applet schema. + """ compiled = SchemaPipeline().compile() - expected = json.loads(COMPILED_SCHEMA_FIXTURE.read_text(encoding="utf-8")) + expected = yaml.safe_load(COMPILED_SCHEMA_SNAPSHOT.read_text(encoding="utf-8")) - assert _schema_data(compiled) == expected + assert snapshot_data(compiled) == expected def test_compiled_roles_match_public_role_constants(self): """Every compiled role has a public constant, and every constant names a compiled role."""