openedx-authz 2.0.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/workflows/ci.yml +1 -1
  2. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/workflows/release.yml +1 -1
  3. {openedx_authz-2.0.0/src/openedx_authz.egg-info → openedx_authz-2.2.0}/PKG-INFO +2 -1
  4. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0017-static-authorization-schema.rst +7 -1
  5. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0018-authorization-schema-lifecycle.rst +19 -0
  6. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0030-tutor-implementation-authz-schema-discovery.rst +1 -1
  7. openedx_authz-2.2.0/docs/references/authorization-schema-json.rst +118 -0
  8. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/references/authorization-schema.rst +1 -1
  9. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/pyproject.toml +1 -0
  10. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/constants/schema.py +3 -0
  11. {openedx_authz-2.0.0/src/openedx_authz → openedx_authz-2.2.0/src/openedx_authz/engine}/schema/authz-schema-v1.json +3 -3
  12. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/discovery.py +91 -2
  13. openedx_authz-2.2.0/src/openedx_authz/engine/schema/issues.py +37 -0
  14. openedx_authz-2.2.0/src/openedx_authz/engine/schema/loading.py +138 -0
  15. openedx_authz-2.2.0/src/openedx_authz/engine/schema/parsing.py +180 -0
  16. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/pipeline.py +14 -5
  17. openedx_authz-2.2.0/src/openedx_authz/engine/schema/syntax.py +58 -0
  18. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/types/__init__.py +4 -0
  19. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/types/loading.py +24 -0
  20. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/validation.py +7 -161
  21. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/settings/common.py +4 -0
  22. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/categories.yaml +6 -0
  23. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/extensions.yaml +5 -0
  24. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/permissions.yaml +15 -0
  25. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/roles.yaml +8 -0
  26. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_parse/malformed.yaml +2 -0
  27. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_semantics/unknown_category.yaml +9 -0
  28. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_structure/items.yml +4 -0
  29. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_structure/priority.yaml +4 -0
  30. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/categories.yaml +6 -0
  31. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/extensions.yaml +5 -0
  32. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/permissions.yml +15 -0
  33. openedx_authz-2.2.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/roles.yaml +8 -0
  34. openedx_authz-2.2.0/src/openedx_authz/tests/integration/test_schema_pipeline.py +117 -0
  35. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/factories.py +6 -0
  36. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_discovery.py +133 -0
  37. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_loading.py +192 -34
  38. openedx_authz-2.2.0/src/openedx_authz/tests/schema/test_parsing.py +50 -0
  39. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_pipeline.py +60 -11
  40. openedx_authz-2.2.0/src/openedx_authz/tests/schema/test_syntax.py +225 -0
  41. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_types.py +12 -0
  42. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_validation.py +8 -146
  43. {openedx_authz-2.0.0 → openedx_authz-2.2.0/src/openedx_authz.egg-info}/PKG-INFO +2 -1
  44. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz.egg-info/SOURCES.txt +31 -3
  45. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz.egg-info/requires.txt +1 -0
  46. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz.egg-info/scm_file_list.json +30 -2
  47. openedx_authz-2.2.0/tutor/tutor-contrib-authz/.gitignore +7 -0
  48. openedx_authz-2.2.0/tutor/tutor-contrib-authz/LICENSE.txt +662 -0
  49. openedx_authz-2.2.0/tutor/tutor-contrib-authz/MANIFEST.in +2 -0
  50. openedx_authz-2.2.0/tutor/tutor-contrib-authz/Makefile +30 -0
  51. openedx_authz-2.2.0/tutor/tutor-contrib-authz/README.rst +80 -0
  52. openedx_authz-2.2.0/tutor/tutor-contrib-authz/pyproject.toml +65 -0
  53. openedx_authz-2.2.0/tutor/tutor-contrib-authz/tutor_authz/__about__.py +1 -0
  54. openedx_authz-2.2.0/tutor/tutor-contrib-authz/tutor_authz/patches/openedx-common-settings +2 -0
  55. openedx_authz-2.2.0/tutor/tutor-contrib-authz/tutor_authz/plugin.py +222 -0
  56. openedx_authz-2.2.0/tutor/tutor-contrib-authz/tutor_authz/templates/apps/openedx/config/authz-schema.yml +9 -0
  57. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/uv.lock +2 -0
  58. openedx_authz-2.0.0/docs/references/authorization-schema-json.rst +0 -118
  59. openedx_authz-2.0.0/src/openedx_authz/engine/schema/loading.py +0 -287
  60. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.annotation_safe_list.yml +0 -0
  61. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.editorconfig +0 -0
  62. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  63. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/dependabot.yml +0 -0
  64. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/workflows/add-depr-ticket-to-depr-board.yml +0 -0
  65. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/workflows/add-remove-label-on-comment.yml +0 -0
  66. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/workflows/commitlint.yml +0 -0
  67. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/workflows/self-assign-issue.yml +0 -0
  68. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.github/workflows/upgrade-python-requirements.yml +0 -0
  69. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.gitignore +0 -0
  70. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.pii_annotations.yml +0 -0
  71. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/.readthedocs.yaml +0 -0
  72. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/CHANGELOG.rst +0 -0
  73. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/LICENSE +0 -0
  74. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/MANIFEST.in +0 -0
  75. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/Makefile +0 -0
  76. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/README.rst +0 -0
  77. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/catalog-info.yaml +0 -0
  78. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/codecov.yml +0 -0
  79. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/Makefile +0 -0
  80. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/_images/architecture-overview.png +0 -0
  81. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/_static/theme_overrides.css +0 -0
  82. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/changelog.rst +0 -0
  83. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/concepts/core_roles_and_permissions/content_library_roles.rst +0 -0
  84. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/concepts/core_roles_and_permissions/course_roles.rst +0 -0
  85. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/concepts/core_roles_and_permissions/index.rst +0 -0
  86. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/concepts/index.rst +0 -0
  87. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/conf.py +0 -0
  88. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0001-purpose-of-this-repo.rst +0 -0
  89. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0002-authorization-model-foundation.rst +0 -0
  90. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0003-jwt-usage.rst +0 -0
  91. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0004-technology-selection.rst +0 -0
  92. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0005-architecture-and-data-modeling.rst +0 -0
  93. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0007-enforcement-mechanisms-mfe.rst +0 -0
  94. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0008-compatibility-scheme.rst +0 -0
  95. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0009-authz-for-course-authoring.rst +0 -0
  96. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0010-course-authoring-flag.rst +0 -0
  97. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0011-course-authoring-migration-process.rst +0 -0
  98. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0012-auditability.rst +0 -0
  99. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0012-glob-support-for-role-assignments.rst +0 -0
  100. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0013-course-authoring-automatic-migration.rst +0 -0
  101. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0014-bulk-assignment-queries-without-casbin-enforce.rst +0 -0
  102. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0015-expose-course-authoring-waffle-flag-state-via-rest-api.rst +0 -0
  103. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0016-static-and-dynamic-roles.rst +0 -0
  104. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0019-authorization-schema-discovery.rst +0 -0
  105. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0020-authorization-schema-internationalization.rst +0 -0
  106. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0021-authorization-definition-api.rst +0 -0
  107. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0022-defer-authorization-implications.rst +0 -0
  108. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0023-extend-static-roles.rst +0 -0
  109. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0024-api-contract-for-user-grouped-role-assignments.rst +0 -0
  110. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0025-authorization-schema-source-tracking.rst +0 -0
  111. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0026-rest-api-domain-ownership-boundary.rst +0 -0
  112. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0027-cross-domain-filtering-via-openedx-filters.rst +0 -0
  113. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0028-api-contract-for-role-catalog.rst +0 -0
  114. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0029-paragon-icon-list-maintenance.rst +0 -0
  115. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/0031-priority-applies-to-role-extensions-only.rst +0 -0
  116. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions/README.rst +0 -0
  117. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/decisions.rst +0 -0
  118. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/getting_started.rst +0 -0
  119. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/how-tos/index.rst +0 -0
  120. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/index.rst +0 -0
  121. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/internationalization.rst +0 -0
  122. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/make.bat +0 -0
  123. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/quickstarts/index.rst +0 -0
  124. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/readme.rst +0 -0
  125. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/references/index.rst +0 -0
  126. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/docs/testing.rst +0 -0
  127. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/manage.py +0 -0
  128. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/pylintrc +0 -0
  129. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/pylintrc_tweaks +0 -0
  130. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/ruff.toml +0 -0
  131. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/scripts/generate_paragon_icons.py +0 -0
  132. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/setup.cfg +0 -0
  133. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/__init__.py +0 -0
  134. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/admin.py +0 -0
  135. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/api/__init__.py +0 -0
  136. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/api/catalog.py +0 -0
  137. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/api/data.py +0 -0
  138. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/api/permissions.py +0 -0
  139. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/api/roles.py +0 -0
  140. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/api/users.py +0 -0
  141. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/api/utils.py +0 -0
  142. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/apps.py +0 -0
  143. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/authz/__init__.py +0 -0
  144. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/authz/schema/course_permissions.yaml +0 -0
  145. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/authz/schema/course_roles.yaml +0 -0
  146. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/authz/schema/library_permissions.yaml +0 -0
  147. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/authz/schema/library_roles.yaml +0 -0
  148. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/conf/locale/config.yaml +0 -0
  149. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/constants/__init__.py +0 -0
  150. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/constants/permissions.py +0 -0
  151. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/constants/roles.py +0 -0
  152. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/data.py +0 -0
  153. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/__init__.py +0 -0
  154. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/adapter.py +0 -0
  155. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/apps.py +0 -0
  156. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/config/authz.policy +0 -0
  157. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/config/model.conf +0 -0
  158. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/enforcer.py +0 -0
  159. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/filter.py +0 -0
  160. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/matcher.py +0 -0
  161. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/policy.py +0 -0
  162. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/renderer.py +0 -0
  163. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/__init__.py +0 -0
  164. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/compilation.py +0 -0
  165. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/exceptions.py +0 -0
  166. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/paragon_icons.py +0 -0
  167. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/translation.py +0 -0
  168. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/types/compilation.py +0 -0
  169. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/schema/types/definitions.py +0 -0
  170. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/engine/utils.py +0 -0
  171. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/filters.py +0 -0
  172. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/handlers.py +0 -0
  173. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/management/__init__.py +0 -0
  174. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/management/commands/__init__.py +0 -0
  175. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/management/commands/authz_migrate_course_authoring.py +0 -0
  176. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/management/commands/authz_rollback_course_authoring.py +0 -0
  177. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/management/commands/enforcement.py +0 -0
  178. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/management/commands/extract_schema_translations.py +0 -0
  179. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/management/commands/load_authz_schema.py +0 -0
  180. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/management/commands/load_policies.py +0 -0
  181. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0001_add_casbin_dependency.py +0 -0
  182. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0002_initial.py +0 -0
  183. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0003_usersubject.py +0 -0
  184. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0004_contentlibraryscope.py +0 -0
  185. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0005_policycachecontrol.py +0 -0
  186. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0006_migrate_legacy_permissions.py +0 -0
  187. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0007_coursescope.py +0 -0
  188. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0008_authzcourseauthoringmigrationrun.py +0 -0
  189. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0009_roleassignmentaudit.py +0 -0
  190. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0010_scope_external_key.py +0 -0
  191. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/0011_authz_schema_definitions.py +0 -0
  192. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/migrations/__init__.py +0 -0
  193. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/models/__init__.py +0 -0
  194. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/models/authz_migration.py +0 -0
  195. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/models/base.py +0 -0
  196. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/models/core.py +0 -0
  197. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/models/engine.py +0 -0
  198. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/models/schema.py +0 -0
  199. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/models/scopes.py +0 -0
  200. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/models/subjects.py +0 -0
  201. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/__init__.py +0 -0
  202. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/data.py +0 -0
  203. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/decorators.py +0 -0
  204. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/urls.py +0 -0
  205. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/utils.py +0 -0
  206. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/__init__.py +0 -0
  207. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/admin_console/__init__.py +0 -0
  208. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/admin_console/filters.py +0 -0
  209. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/admin_console/serializers.py +0 -0
  210. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/admin_console/views.py +0 -0
  211. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/course_authoring/__init__.py +0 -0
  212. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/course_authoring/pipeline.py +0 -0
  213. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/course_authoring/views.py +0 -0
  214. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/fields.py +0 -0
  215. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/paginators.py +0 -0
  216. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/permissions.py +0 -0
  217. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/serializers.py +0 -0
  218. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/urls.py +0 -0
  219. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/rest_api/v1/views.py +0 -0
  220. {openedx_authz-2.0.0/src/openedx_authz/schema → openedx_authz-2.2.0/src/openedx_authz/settings}/__init__.py +0 -0
  221. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/settings/production.py +0 -0
  222. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/settings/test.py +0 -0
  223. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/templates/openedx_authz/base.html +0 -0
  224. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/__init__.py +0 -0
  225. {openedx_authz-2.0.0/src/openedx_authz/settings → openedx_authz-2.2.0/src/openedx_authz/tests/api}/__init__.py +0 -0
  226. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/api/test_catalog.py +0 -0
  227. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/api/test_data.py +0 -0
  228. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/api/test_roles.py +0 -0
  229. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/api/test_users.py +0 -0
  230. {openedx_authz-2.0.0/src/openedx_authz/tests/api → openedx_authz-2.2.0/src/openedx_authz/tests/authz}/__init__.py +0 -0
  231. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/authz/test_resources.py +0 -0
  232. {openedx_authz-2.0.0/src/openedx_authz/tests/authz → openedx_authz-2.2.0/src/openedx_authz/tests/integration}/__init__.py +0 -0
  233. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/integration/conftest.py +0 -0
  234. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/integration/test_handlers.py +0 -0
  235. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/integration/test_models.py +0 -0
  236. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/integration/test_schema_apply.py +0 -0
  237. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/integration/test_views.py +0 -0
  238. {openedx_authz-2.0.0/src/openedx_authz/tests/integration → openedx_authz-2.2.0/src/openedx_authz/tests/rest_api}/__init__.py +0 -0
  239. {openedx_authz-2.0.0/src/openedx_authz/tests/rest_api → openedx_authz-2.2.0/src/openedx_authz/tests/rest_api/admin_console}/__init__.py +0 -0
  240. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/rest_api/admin_console/test_views.py +0 -0
  241. {openedx_authz-2.0.0/src/openedx_authz/tests/rest_api/admin_console → openedx_authz-2.2.0/src/openedx_authz/tests/rest_api/course_authoring}/__init__.py +0 -0
  242. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/rest_api/course_authoring/test_pipeline.py +0 -0
  243. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/rest_api/course_authoring/test_views.py +0 -0
  244. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/rest_api/mixins.py +0 -0
  245. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/rest_api/test_permissions.py +0 -0
  246. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/rest_api/test_utils.py +0 -0
  247. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/rest_api/test_views.py +0 -0
  248. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/__init__.py +0 -0
  249. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_apply.py +0 -0
  250. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_compilation.py +0 -0
  251. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_load_authz_schema_command.py +0 -0
  252. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_renderer.py +0 -0
  253. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_schema_models.py +0 -0
  254. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_source_storage.py +0 -0
  255. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/schema/test_translation.py +0 -0
  256. {openedx_authz-2.0.0/src/openedx_authz/tests/rest_api/course_authoring → openedx_authz-2.2.0/src/openedx_authz/tests/stubs}/__init__.py +0 -0
  257. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/stubs/apps.py +0 -0
  258. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/stubs/migrations/0001_initial.py +0 -0
  259. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/stubs/migrations/0002_learningpackage_contentlibrary_learning_package.py +0 -0
  260. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/stubs/migrations/0002_userprofile.py +0 -0
  261. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/stubs/models.py +0 -0
  262. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_commands.py +0 -0
  263. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_enforcement.py +0 -0
  264. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_enforcer.py +0 -0
  265. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_engine_utils.py +0 -0
  266. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_filter.py +0 -0
  267. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_handlers.py +0 -0
  268. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_imports.py +0 -0
  269. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_migrations.py +0 -0
  270. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_models.py +0 -0
  271. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_policy.py +0 -0
  272. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/tests/test_utils.py +0 -0
  273. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/urls.py +0 -0
  274. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz/utils.py +0 -0
  275. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz.egg-info/dependency_links.txt +0 -0
  276. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz.egg-info/entry_points.txt +0 -0
  277. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/src/openedx_authz.egg-info/top_level.txt +0 -0
  278. {openedx_authz-2.0.0 → openedx_authz-2.2.0}/tox.ini +0 -0
  279. {openedx_authz-2.0.0/src/openedx_authz/tests/stubs → openedx_authz-2.2.0/tutor/tutor-contrib-authz/tutor_authz}/__init__.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
22
 
23
23
  - name: Install uv
24
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
24
+ uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
25
25
  with:
26
26
  enable-cache: true
27
27
  python-version: ${{ matrix.python-version }}
@@ -87,4 +87,4 @@ jobs:
87
87
  path: dist/
88
88
 
89
89
  - name: Publish to PyPI
90
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
90
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openedx-authz
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Open edX AuthZ provides the architecture and foundations of the authorization framework.
5
5
  Author-email: Open edX Project <oscm@openedx.org>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -28,6 +28,7 @@ Requires-Dist: edx-django-utils
28
28
  Requires-Dist: edx-drf-extensions
29
29
  Requires-Dist: edx-opaque-keys
30
30
  Requires-Dist: edx-organizations
31
+ Requires-Dist: jsonschema>=4.0
31
32
  Requires-Dist: openedx-atlas
32
33
  Requires-Dist: openedx-events
33
34
  Requires-Dist: openedx-filters>=3.15.0
@@ -19,7 +19,7 @@ Decision
19
19
  1. Schema format and boundary
20
20
  =============================
21
21
 
22
- The authz schema is a versioned YAML format for static permissions, permission categories, roles, and changes to existing roles. Every file declares ``schema_version`` and ``priority``, which apply to all definitions in that file. Priority is used only when role extensions conflict; it does not resolve conflicting base definitions (see `ADR 0031`_). Open edX publishes a :download:`machine-readable JSON Schema <../../src/openedx_authz/schema/authz-schema-v1.json>` for this format so that editors, CI, and the compiler all apply the same field and validation rules.
22
+ The authz schema is a versioned YAML format for static permissions, permission categories, roles, and changes to existing roles. Every file declares ``schema_version`` and ``priority``, which apply to all definitions in that file. Priority is used only when role extensions conflict; it does not resolve conflicting base definitions (see `ADR 0031`_). Open edX publishes a :download:`machine-readable JSON Schema <../../src/openedx_authz/engine/schema/authz-schema-v1.json>` for this format so that editors, CI, and the compiler all apply the same field and validation rules.
23
23
 
24
24
  The existing static role and permission definitions in Python modules and ``authz.policy`` will move into the schema. Once this migration is complete, the schema becomes the source for static definitions, so developers add a new role or permission there without duplicating it in Python constants or policy files.
25
25
 
@@ -155,6 +155,12 @@ Embedded categories on every permission
155
155
 
156
156
  Repeating category labels makes localization and consistent presentation harder. A normalized category gives permissions one stable grouping reference.
157
157
 
158
+ Addendum: 2026-10-20
159
+ ********************
160
+
161
+ The machine-readable JSON Schema now lives with its validation engine at
162
+ :download:`authz-schema-v1.json <../../src/openedx_authz/engine/schema/authz-schema-v1.json>`.
163
+
158
164
  References
159
165
  **********
160
166
 
@@ -107,6 +107,25 @@ Two limitations are accepted:
107
107
  * **Single-database assumption.** Serialization holds only when the generated policy and the lock live in the same database. The engine already assumes one database for its policy storage, so this is consistent with the rest of the system.
108
108
  * **Blocking wait without observability.** A second apply waits for the first with no timeout and no distinct signal, so a blocked apply is operationally indistinguishable from a slow one. This is acceptable because apply runs at deployment time, not on the request path.
109
109
 
110
+ Addendum: Separate structural and semantic validation
111
+ ======================================================
112
+
113
+ **2026-10-20**
114
+
115
+ Schema processing separates parsing, structural validation, typed loading, and semantic validation into ordered phases.
116
+
117
+ The lifecycle begins with these phases:
118
+
119
+ 1. **Discover** identifies the contributed schema resources.
120
+ 2. **Parse** reads each resource as YAML and preserves the resulting value with its source information.
121
+ 3. **Validate structure** checks every parsed value against the configured JSON Schema. The pipeline reports all structural errors and stops when it finds any.
122
+ 4. **Load** constructs typed schema definitions from structurally valid mappings.
123
+ 5. **Validate semantics** checks relationships across definitions, conflicts between contributions, supported scopes, and values that depend on application data such as the vendored Paragon icon names.
124
+
125
+ Compile, render, apply, and consume keep the responsibilities defined above. Typed loading and semantic validation do not run when structural validation fails. This ordering lets JSON Schema report malformed collection shapes and value types before typed construction assumes those structures are valid.
126
+
127
+ The separation adds an intermediate parsed-document representation and another explicit pipeline boundary. Callers that previously treated loading as both YAML parsing and typed construction must use the corresponding phase for the representation they need.
128
+
110
129
  Consequences
111
130
  ************
112
131
 
@@ -102,7 +102,7 @@ Example rendered file with two plugin contributions:
102
102
  Schemas discovered through ``OPENEDX_AUTHZ_SCHEMA_FILES`` have no Python package or module. To satisfy the source-tracking requirements of `ADR 0025`_, their source record uses:
103
103
 
104
104
  * ``distribution`` = ``<tutor-patch>`` — a sentinel value that is an invalid Python package name, guaranteeing no collision with any installed distribution.
105
- * ``module`` = ``null`` — filesystem sources have no owning Python module, and using the absolute path would be inconsistent with the field's semantics elsewhere in the model.
105
+ * ``module`` = the absolute filesystem path of the schema file (e.g. ``/openedx/config/authz-schema.yml``). Filesystem sources have no owning Python module, but reusing the path here keeps the ``(distribution, module)`` unique constraint enforceable and avoids the ``NULL``-handling complications that a ``null`` value would introduce across the ORM, migrations, and source-identity logic.
106
106
  * ``resource_path`` = the absolute filesystem path of the schema file (e.g. ``/openedx/config/authz-schema.yml``).
107
107
  * ``distribution_version`` = ``0.0.0`` — a sentinel value, since filesystem sources carry no package version.
108
108
 
@@ -0,0 +1,118 @@
1
+ .. _Generated Authorization Schema:
2
+
3
+ Generated Authorization Schema
4
+ ##############################
5
+
6
+ This page renders the exact validation rules from the :download:`authz JSON Schema <../../src/openedx_authz/engine/schema/authz-schema-v1.json>`. See the :doc:`Authorization Schema Reference <authorization-schema>` for guidance, examples, and validation performed by the compiler across schema contributions.
7
+
8
+ Top-level fields
9
+ ****************
10
+
11
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/properties/schema_version
12
+ :lift_description:
13
+
14
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/properties/priority
15
+ :lift_description:
16
+
17
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/properties/permission_categories
18
+ :lift_description:
19
+ :hide_key: /items
20
+
21
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/properties/permissions
22
+ :lift_description:
23
+ :hide_key: /items
24
+
25
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/properties/roles
26
+ :lift_description:
27
+ :hide_key: /items
28
+
29
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/properties/role_extensions
30
+ :lift_description:
31
+ :hide_key: /items
32
+
33
+ Permission category fields
34
+ **************************
35
+
36
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission_category/properties/id
37
+ :lift_description:
38
+
39
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission_category/properties/display_name
40
+ :lift_description:
41
+
42
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission_category/properties/description
43
+ :lift_description:
44
+
45
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission_category/properties/icon
46
+ :lift_description:
47
+
48
+ Permission fields
49
+ *****************
50
+
51
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission/properties/namespace
52
+ :lift_description:
53
+
54
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission/properties/name
55
+ :lift_description:
56
+
57
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission/properties/display_name
58
+ :lift_description:
59
+
60
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission/properties/description
61
+ :lift_description:
62
+
63
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission/properties/category_id
64
+ :lift_description:
65
+
66
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission/properties/scopes
67
+ :lift_description:
68
+
69
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/permission/properties/icon
70
+ :lift_description:
71
+
72
+ Role fields
73
+ ***********
74
+
75
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role/properties/id
76
+ :lift_description:
77
+
78
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role/properties/display_name
79
+ :lift_description:
80
+
81
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role/properties/description
82
+ :lift_description:
83
+
84
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role/properties/scopes
85
+ :lift_description:
86
+
87
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role/properties/permissions
88
+ :lift_description:
89
+
90
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role/properties/icon
91
+ :lift_description:
92
+
93
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role/properties/hidden
94
+ :lift_description:
95
+
96
+ Role extension fields
97
+ *********************
98
+
99
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role_extension/properties/role_id
100
+ :lift_description:
101
+
102
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role_extension/properties/add_permissions
103
+ :lift_description:
104
+
105
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role_extension/properties/remove_permissions
106
+ :lift_description:
107
+
108
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role_extension/properties/display_name
109
+ :lift_description:
110
+
111
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role_extension/properties/description
112
+ :lift_description:
113
+
114
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role_extension/properties/icon
115
+ :lift_description:
116
+
117
+ .. jsonschema:: ../../src/openedx_authz/engine/schema/authz-schema-v1.json#/$defs/role_extension/properties/hidden
118
+ :lift_description:
@@ -19,7 +19,7 @@ Use this reference when creating or reviewing an authz schema file. The examples
19
19
  Machine-readable schema
20
20
  ***********************
21
21
 
22
- The :download:`downloadable authz JSON Schema <../../src/openedx_authz/schema/authz-schema-v1.json>` describes the fields, types, required values, and identifier formats for schema version ``1.0``. You can also :doc:`browse the generated schema <authorization-schema-json>` or `view the schema source <https://github.com/openedx/openedx-authz/blob/main/src/openedx_authz/schema/authz-schema-v1.json>`_. Editors and validation tools can use it to check YAML files because JSON Schema applies to the data represented by both JSON and YAML.
22
+ The :download:`downloadable authz JSON Schema <../../src/openedx_authz/engine/schema/authz-schema-v1.json>` describes the fields, types, required values, and identifier formats for schema version ``1.0``. You can also :doc:`browse the generated schema <authorization-schema-json>` or `view the schema source <https://github.com/openedx/openedx-authz/blob/main/src/openedx_authz/engine/schema/authz-schema-v1.json>`_. Editors and validation tools can use it to check YAML files because JSON Schema applies to the data represented by both JSON and YAML.
23
23
 
24
24
  The JSON Schema checks the structure of one file. The compiler also checks references across the combined schema, registered scope namespaces, available Paragon icons, and conflicts between contributions.
25
25
 
@@ -36,6 +36,7 @@ dependencies = [
36
36
  "edx-drf-extensions",
37
37
  "edx-opaque-keys",
38
38
  "edx-organizations",
39
+ "jsonschema>=4.0",
39
40
  "openedx-atlas",
40
41
  "openedx-events",
41
42
  "openedx-filters>=3.15.0",
@@ -12,6 +12,9 @@ from __future__ import annotations
12
12
 
13
13
  from enum import Enum
14
14
 
15
+ AUTHZ_JSON_SCHEMA_SETTING = "OPENEDX_AUTHZ_JSON_SCHEMA"
16
+ DEFAULT_AUTHZ_JSON_SCHEMA = "openedx_authz.engine.schema/authz-schema-v1.json"
17
+
15
18
 
16
19
  class SchemaOriginKind(str, Enum):
17
20
  """Whether a schema contribution is a base definition or an extension.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/openedx/openedx-authz/main/src/openedx_authz/schema/authz-schema-v1.json",
3
+ "$id": "https://raw.githubusercontent.com/openedx/openedx-authz/main/src/openedx_authz/engine/schema/authz-schema-v1.json",
4
4
  "title": "Open edX Authorization schema",
5
5
  "description": "A version 1.0 Open edX authorization schema contribution. Applications and deployment configuration use this format to define static permission categories, permissions, roles, and changes to existing roles.",
6
6
  "type": "object",
@@ -121,7 +121,7 @@
121
121
  "scope_namespace": {
122
122
  "description": "A scope namespace registered by a ScopeData type.",
123
123
  "type": "string",
124
- "minLength": 1,
124
+ "pattern": "^[a-z][a-z0-9_-]*$",
125
125
  "examples": ["course-v1"]
126
126
  },
127
127
  "display_name": {
@@ -139,7 +139,7 @@
139
139
  "icon": {
140
140
  "description": "A case-sensitive icon name exported by @openedx/paragon/icons.",
141
141
  "type": "string",
142
- "minLength": 1,
142
+ "pattern": "^[A-Z][A-Za-z0-9]*$",
143
143
  "examples": ["Visibility"]
144
144
  },
145
145
  "permission_list": {
@@ -1,7 +1,7 @@
1
1
  """Discover static authz schema resources (the ``discover`` step, ADR 0019).
2
2
 
3
- Providers register **directories** (not individual files); the loader reads
4
- every ``.yaml`` file inside them. Two contribution sources are merged:
3
+ Providers register **directories** of ``.yaml`` files, or individual absolute
4
+ filesystem paths. Three contribution sources are merged:
5
5
 
6
6
  1. The ``authz.schema`` entry-point group. Each registered callable returns
7
7
  directory paths in the package-anchored format below (e.g.
@@ -9,6 +9,10 @@ every ``.yaml`` file inside them. Two contribution sources are merged:
9
9
  2. The ``OPENEDX_AUTHZ_SCHEMA_DIRECTORIES`` Django setting, a list of directory
10
10
  path strings in the same format. This lets operators and CI contribute
11
11
  directories without shipping a package entry point.
12
+ 3. The ``OPENEDX_AUTHZ_SCHEMA_FILES`` Django setting, a list of absolute
13
+ filesystem paths each naming one schema file (ADR 0030). This is the route
14
+ used by the Tutor ``openedx-authz-schema`` patch to feed a rendered file
15
+ into discovery without anchoring it on a Python package.
12
16
 
13
17
  Directory format: these are **not** filesystem paths (neither relative nor
14
18
  absolute). Each is a package-anchored ``importlib.resources`` path: the first
@@ -36,12 +40,15 @@ import logging
36
40
  from dataclasses import dataclass
37
41
  from enum import StrEnum
38
42
  from importlib import metadata, resources
43
+ from pathlib import Path
39
44
 
40
45
  from django.conf import settings
41
46
  from django.core.exceptions import ImproperlyConfigured
42
47
 
43
48
  ENTRY_POINT_GROUP = "authz.schema"
44
49
  SETTINGS_DIRECTORIES_NAME = "OPENEDX_AUTHZ_SCHEMA_DIRECTORIES"
50
+ SETTINGS_FILE_SENTINEL = "<tutor-patch>"
51
+ SETTINGS_FILE_VERSION = "0.0.0"
45
52
  SCHEMA_FILE_SUFFIXES = (".yaml", ".yml")
46
53
 
47
54
  logger = logging.getLogger(__name__)
@@ -101,6 +108,45 @@ class DiscoveredResource:
101
108
  ) from exc
102
109
 
103
110
 
111
+ @dataclass(frozen=True)
112
+ class DiscoveredFileResource(DiscoveredResource):
113
+ """A schema file discovered via an absolute filesystem path (ADR 0030).
114
+
115
+ Unlike :class:`DiscoveredResource`, which resolves through
116
+ ``importlib.resources``, this variant reads directly from the filesystem.
117
+ Fields are populated with sentinel values: ``package`` is
118
+ :data:`SETTINGS_FILE_SENTINEL` and both ``module`` and ``resource_path``
119
+ hold the absolute path.
120
+ """
121
+
122
+ @classmethod
123
+ def from_path(cls, file_path: str) -> DiscoveredFileResource:
124
+ """Build a resource from an absolute filesystem path.
125
+
126
+ Sentinel values are used for ``package`` and ``module`` because the
127
+ file is not anchored on any Python package.
128
+ """
129
+ return cls(
130
+ package=SETTINGS_FILE_SENTINEL,
131
+ resource_path=file_path,
132
+ module=file_path,
133
+ origin=Origin.SETTINGS,
134
+ )
135
+
136
+ def read_bytes(self) -> bytes:
137
+ """Read the file directly from the filesystem via :class:`pathlib.Path`.
138
+
139
+ Raises:
140
+ SchemaDiscoveryError: If the file cannot be read.
141
+ """
142
+ try:
143
+ return Path(self.resource_path).read_bytes()
144
+ except OSError as exc:
145
+ raise SchemaDiscoveryError(
146
+ f"Could not read schema file {self.resource_path!r}: {exc}"
147
+ ) from exc
148
+
149
+
104
150
  class SchemaDiscoveryError(Exception):
105
151
  """Raised when a provider fails or a declared directory cannot be read."""
106
152
 
@@ -139,6 +185,7 @@ class SchemaDiscovery:
139
185
  found: list[DiscoveredResource] = []
140
186
  found.extend(self._discover_entry_points())
141
187
  found.extend(self._discover_settings_directories())
188
+ found.extend(self._discover_settings_files())
142
189
  for directory in self._passed_in_directories:
143
190
  found.extend(self._iter_directory(directory, origin=Origin.PASSED_IN))
144
191
 
@@ -184,6 +231,48 @@ class SchemaDiscovery:
184
231
  discovered.extend(self._iter_directory(directory, origin=Origin.SETTINGS))
185
232
  return discovered
186
233
 
234
+ def _discover_settings_files(self) -> list[DiscoveredResource]:
235
+ """Read ``OPENEDX_AUTHZ_SCHEMA_FILES`` from Django settings (ADR 0030).
236
+
237
+ Each item is an absolute filesystem path to a single schema file.
238
+ Unlike ``_discover_settings_directories``, every listed path must exist
239
+ and have a supported extension; a missing or misnamed file is treated
240
+ as a misconfiguration and raises ``SchemaDiscoveryError``.
241
+
242
+ Absent, empty, or unconfigured settings yield nothing. As with
243
+ ``_discover_settings_directories``, ``ImproperlyConfigured`` degrades
244
+ to no contribution so CI can run outside a Django process.
245
+ """
246
+ try:
247
+ file_paths = getattr(settings, "OPENEDX_AUTHZ_SCHEMA_FILES", None) or []
248
+ except ImproperlyConfigured:
249
+ return []
250
+ if not isinstance(file_paths, list):
251
+ raise SchemaDiscoveryError(
252
+ "OPENEDX_AUTHZ_SCHEMA_FILES must be a list of filesystem paths, "
253
+ f"got {type(file_paths).__name__!r}."
254
+ )
255
+ discovered: list[DiscoveredResource] = []
256
+ for file_path in file_paths:
257
+ if not file_path.endswith(SCHEMA_FILE_SUFFIXES):
258
+ raise SchemaDiscoveryError(
259
+ f"Schema file {file_path!r} contributed through "
260
+ "OPENEDX_AUTHZ_SCHEMA_FILES has an unsupported file extension, "
261
+ f"accepted extensions: {SCHEMA_FILE_SUFFIXES}"
262
+ )
263
+
264
+ path = Path(file_path)
265
+ if not path.is_file():
266
+ raise SchemaDiscoveryError(
267
+ f"Schema file {file_path!r} contributed through "
268
+ "OPENEDX_AUTHZ_SCHEMA_FILES is not an actual file"
269
+ )
270
+
271
+ discovered.append(
272
+ DiscoveredFileResource.from_path(file_path)
273
+ )
274
+ return discovered
275
+
187
276
  def _iter_directory(self, directory: str, *, origin: Origin) -> list[DiscoveredResource]:
188
277
  """Resolve a directory path and return one resource per ``.yaml`` file.
189
278
 
@@ -0,0 +1,37 @@
1
+ """Validation finding types shared by structural and semantic checks."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+
6
+
7
+ class IssueLevel(StrEnum):
8
+ """Severity of a :class:`ValidationIssue`.
9
+
10
+ Members:
11
+ ERROR: Blocks deployment.
12
+ WARNING: Reported only, does not block deployment.
13
+ """
14
+
15
+ ERROR = "error"
16
+ WARNING = "warning"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ValidationIssue:
21
+ """A single validation finding.
22
+
23
+ Attributes:
24
+ level: :attr:`IssueLevel.ERROR` (blocks deployment) or
25
+ :attr:`IssueLevel.WARNING` (reported only).
26
+ message: Human-readable description.
27
+ source_id: The contributing source, when the issue is file-specific.
28
+ """
29
+
30
+ level: IssueLevel
31
+ message: str
32
+ source_id: str | None = None
33
+
34
+ @property
35
+ def is_error(self) -> bool:
36
+ """Return whether this finding blocks the pipeline."""
37
+ return self.level == IssueLevel.ERROR
@@ -0,0 +1,138 @@
1
+ """Convert parsed mappings into schema documents (the ``load`` step, ADR 0018).
2
+
3
+ Each parsed document already contains its YAML mapping and source record. This
4
+ step performs only typed structural shaping. Semantic checks belong to
5
+ :mod:`.validation` and cross-file resolution belongs to :mod:`.compilation`.
6
+
7
+ No Casbin or Django imports, so it stays unit-testable in isolation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from openedx_authz.engine.schema.exceptions import SchemaLoadError
13
+ from openedx_authz.engine.schema.types import (
14
+ ParsedSchemaDocument,
15
+ PermissionCategory,
16
+ PermissionDefinition,
17
+ RoleDefinition,
18
+ RoleExtension,
19
+ SchemaDocument,
20
+ SourceRecord,
21
+ )
22
+
23
+
24
+ class SchemaLoader:
25
+ """Turn structurally valid parsed documents into typed schema documents.
26
+
27
+ Resource reading, YAML parsing, content digests, and distribution metadata
28
+ belong to :class:`~openedx_authz.engine.schema.parsing.SchemaParser`. This
29
+ class owns only the conversion into typed definitions.
30
+ """
31
+
32
+ def load(self, documents: list[ParsedSchemaDocument]) -> list[SchemaDocument]:
33
+ """Load every parsed mapping into a :class:`SchemaDocument`.
34
+
35
+ Raises:
36
+ SchemaLoadError: If called with a root value that is not a mapping.
37
+ """
38
+ loaded = []
39
+ for document in documents:
40
+ if not isinstance(document.data, dict):
41
+ raise SchemaLoadError(f"{document.source.source_id}: schema document must be a mapping.")
42
+ loaded.append(self._build_document(document.data, document.source))
43
+ return loaded
44
+
45
+ def _build_document(self, raw: dict, source: SourceRecord) -> SchemaDocument:
46
+ """Map the parsed mapping's blocks into a typed SchemaDocument.
47
+
48
+ ``priority`` is a *required* field per ``authz-schema-v1.json``; a higher
49
+ value wins when contributions conflict (see
50
+ ``docs/references/authorization-schema.rst``). JSON Schema validates
51
+ its presence and type before this method runs. The fallback to ``0``
52
+ and conversion error remain defensive behavior for direct loader use.
53
+ """
54
+ try:
55
+ priority = int(raw.get("priority", 0))
56
+ except (TypeError, ValueError) as exc:
57
+ raise SchemaLoadError(
58
+ f"{source.source_id}: 'priority' must be an integer, got {raw.get('priority')!r}."
59
+ ) from exc
60
+
61
+ return SchemaDocument(
62
+ source=source,
63
+ priority=priority,
64
+ categories=[self._build_category(item, source) for item in raw.get("permission_categories", []) or []],
65
+ permissions=[self._build_permission(item, source) for item in raw.get("permissions", []) or []],
66
+ roles=[self._build_role(item, source) for item in raw.get("roles", []) or []],
67
+ role_extensions=[self._build_extension(item, source) for item in raw.get("role_extensions", []) or []],
68
+ )
69
+
70
+ @staticmethod
71
+ def _as_tuple(value) -> tuple[str, ...]:
72
+ """Coerce a YAML list (or None) into a tuple of strings."""
73
+ if not value:
74
+ return ()
75
+ if isinstance(value, str):
76
+ return (value,)
77
+ return tuple(str(item) for item in value)
78
+
79
+ def _build_category(self, item: dict, source: SourceRecord) -> PermissionCategory:
80
+ """Build a ``PermissionCategory`` from one ``permission_categories`` entry."""
81
+ self._require_mapping(item, "permission_categories", source)
82
+ return PermissionCategory(
83
+ id=item.get("id", ""),
84
+ display_name=item.get("display_name", ""),
85
+ description=item.get("description", ""),
86
+ icon=item.get("icon"),
87
+ )
88
+
89
+ def _build_permission(self, item: dict, source: SourceRecord) -> PermissionDefinition:
90
+ """Build a ``PermissionDefinition`` from one ``permissions`` entry."""
91
+ self._require_mapping(item, "permissions", source)
92
+ return PermissionDefinition(
93
+ namespace=item.get("namespace", ""),
94
+ name=item.get("name", ""),
95
+ display_name=item.get("display_name", ""),
96
+ description=item.get("description", ""),
97
+ category_id=item.get("category_id", ""),
98
+ scopes=self._as_tuple(item.get("scopes")),
99
+ icon=item.get("icon"),
100
+ )
101
+
102
+ def _build_role(self, item: dict, source: SourceRecord) -> RoleDefinition:
103
+ """Build a ``RoleDefinition`` from one ``roles`` entry."""
104
+ self._require_mapping(item, "roles", source)
105
+ return RoleDefinition(
106
+ id=item.get("id", ""),
107
+ display_name=item.get("display_name", ""),
108
+ description=item.get("description", ""),
109
+ scopes=self._as_tuple(item.get("scopes")),
110
+ permissions=self._as_tuple(item.get("permissions")),
111
+ icon=item.get("icon"),
112
+ hidden=bool(item.get("hidden", False)),
113
+ )
114
+
115
+ def _build_extension(self, item: dict, source: SourceRecord) -> RoleExtension:
116
+ """Build a ``RoleExtension`` from one ``role_extensions`` entry.
117
+
118
+ ``hidden`` is read as tri-state: absent stays ``None`` ("leave
119
+ unchanged"), distinct from an explicit ``False`` (ADR 0023 §1).
120
+ """
121
+ self._require_mapping(item, "role_extensions", source)
122
+ return RoleExtension(
123
+ role_id=item.get("role_id", ""),
124
+ add_permissions=self._as_tuple(item.get("add_permissions")),
125
+ remove_permissions=self._as_tuple(item.get("remove_permissions")),
126
+ display_name=item.get("display_name"),
127
+ description=item.get("description"),
128
+ icon=item.get("icon"),
129
+ hidden=item.get("hidden"), # tri-state: None means "leave unchanged"
130
+ )
131
+
132
+ @staticmethod
133
+ def _require_mapping(item, block: str, source: SourceRecord) -> None:
134
+ """Raise ``SchemaLoadError`` (naming the block and source) if ``item`` is not a mapping."""
135
+ if not isinstance(item, dict):
136
+ raise SchemaLoadError(
137
+ f"{source.source_id}: each entry in '{block}' must be a mapping, got {type(item).__name__}."
138
+ )