openedx-authz 2.0.0__tar.gz → 2.1.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 (269) hide show
  1. {openedx_authz-2.0.0/src/openedx_authz.egg-info → openedx_authz-2.1.0}/PKG-INFO +2 -1
  2. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0017-static-authorization-schema.rst +7 -1
  3. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0018-authorization-schema-lifecycle.rst +19 -0
  4. openedx_authz-2.1.0/docs/references/authorization-schema-json.rst +118 -0
  5. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/references/authorization-schema.rst +1 -1
  6. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/pyproject.toml +1 -0
  7. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/constants/schema.py +3 -0
  8. {openedx_authz-2.0.0/src/openedx_authz → openedx_authz-2.1.0/src/openedx_authz/engine}/schema/authz-schema-v1.json +3 -3
  9. openedx_authz-2.1.0/src/openedx_authz/engine/schema/issues.py +37 -0
  10. openedx_authz-2.1.0/src/openedx_authz/engine/schema/loading.py +138 -0
  11. openedx_authz-2.1.0/src/openedx_authz/engine/schema/parsing.py +169 -0
  12. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/pipeline.py +14 -5
  13. openedx_authz-2.1.0/src/openedx_authz/engine/schema/syntax.py +58 -0
  14. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/types/__init__.py +2 -0
  15. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/types/loading.py +8 -0
  16. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/validation.py +7 -161
  17. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/settings/common.py +4 -0
  18. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/categories.yaml +6 -0
  19. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/extensions.yaml +5 -0
  20. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/permissions.yaml +15 -0
  21. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/roles.yaml +8 -0
  22. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_parse/malformed.yaml +2 -0
  23. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_semantics/unknown_category.yaml +9 -0
  24. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_structure/items.yml +4 -0
  25. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_structure/priority.yaml +4 -0
  26. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/categories.yaml +6 -0
  27. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/extensions.yaml +5 -0
  28. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/permissions.yml +15 -0
  29. openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/roles.yaml +8 -0
  30. openedx_authz-2.1.0/src/openedx_authz/tests/integration/test_schema_pipeline.py +117 -0
  31. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/factories.py +6 -0
  32. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_loading.py +31 -35
  33. openedx_authz-2.1.0/src/openedx_authz/tests/schema/test_parsing.py +51 -0
  34. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_pipeline.py +60 -11
  35. openedx_authz-2.1.0/src/openedx_authz/tests/schema/test_syntax.py +225 -0
  36. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_types.py +12 -0
  37. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_validation.py +8 -146
  38. {openedx_authz-2.0.0 → openedx_authz-2.1.0/src/openedx_authz.egg-info}/PKG-INFO +2 -1
  39. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/SOURCES.txt +19 -2
  40. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/requires.txt +1 -0
  41. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/scm_file_list.json +19 -2
  42. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/uv.lock +2 -0
  43. openedx_authz-2.0.0/docs/references/authorization-schema-json.rst +0 -118
  44. openedx_authz-2.0.0/src/openedx_authz/engine/schema/loading.py +0 -287
  45. openedx_authz-2.0.0/src/openedx_authz/tests/stubs/__init__.py +0 -0
  46. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.annotation_safe_list.yml +0 -0
  47. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.editorconfig +0 -0
  48. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  49. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/dependabot.yml +0 -0
  50. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/add-depr-ticket-to-depr-board.yml +0 -0
  51. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/add-remove-label-on-comment.yml +0 -0
  52. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/ci.yml +0 -0
  53. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/commitlint.yml +0 -0
  54. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/release.yml +0 -0
  55. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/self-assign-issue.yml +0 -0
  56. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/upgrade-python-requirements.yml +0 -0
  57. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.gitignore +0 -0
  58. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.pii_annotations.yml +0 -0
  59. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.readthedocs.yaml +0 -0
  60. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/CHANGELOG.rst +0 -0
  61. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/LICENSE +0 -0
  62. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/MANIFEST.in +0 -0
  63. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/Makefile +0 -0
  64. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/README.rst +0 -0
  65. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/catalog-info.yaml +0 -0
  66. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/codecov.yml +0 -0
  67. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/Makefile +0 -0
  68. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/_images/architecture-overview.png +0 -0
  69. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/_static/theme_overrides.css +0 -0
  70. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/changelog.rst +0 -0
  71. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/concepts/core_roles_and_permissions/content_library_roles.rst +0 -0
  72. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/concepts/core_roles_and_permissions/course_roles.rst +0 -0
  73. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/concepts/core_roles_and_permissions/index.rst +0 -0
  74. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/concepts/index.rst +0 -0
  75. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/conf.py +0 -0
  76. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0001-purpose-of-this-repo.rst +0 -0
  77. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0002-authorization-model-foundation.rst +0 -0
  78. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0003-jwt-usage.rst +0 -0
  79. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0004-technology-selection.rst +0 -0
  80. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0005-architecture-and-data-modeling.rst +0 -0
  81. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0007-enforcement-mechanisms-mfe.rst +0 -0
  82. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0008-compatibility-scheme.rst +0 -0
  83. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0009-authz-for-course-authoring.rst +0 -0
  84. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0010-course-authoring-flag.rst +0 -0
  85. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0011-course-authoring-migration-process.rst +0 -0
  86. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0012-auditability.rst +0 -0
  87. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0012-glob-support-for-role-assignments.rst +0 -0
  88. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0013-course-authoring-automatic-migration.rst +0 -0
  89. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0014-bulk-assignment-queries-without-casbin-enforce.rst +0 -0
  90. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0015-expose-course-authoring-waffle-flag-state-via-rest-api.rst +0 -0
  91. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0016-static-and-dynamic-roles.rst +0 -0
  92. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0019-authorization-schema-discovery.rst +0 -0
  93. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0020-authorization-schema-internationalization.rst +0 -0
  94. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0021-authorization-definition-api.rst +0 -0
  95. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0022-defer-authorization-implications.rst +0 -0
  96. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0023-extend-static-roles.rst +0 -0
  97. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0024-api-contract-for-user-grouped-role-assignments.rst +0 -0
  98. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0025-authorization-schema-source-tracking.rst +0 -0
  99. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0026-rest-api-domain-ownership-boundary.rst +0 -0
  100. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0027-cross-domain-filtering-via-openedx-filters.rst +0 -0
  101. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0028-api-contract-for-role-catalog.rst +0 -0
  102. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0029-paragon-icon-list-maintenance.rst +0 -0
  103. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0030-tutor-implementation-authz-schema-discovery.rst +0 -0
  104. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0031-priority-applies-to-role-extensions-only.rst +0 -0
  105. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/README.rst +0 -0
  106. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions.rst +0 -0
  107. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/getting_started.rst +0 -0
  108. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/how-tos/index.rst +0 -0
  109. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/index.rst +0 -0
  110. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/internationalization.rst +0 -0
  111. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/make.bat +0 -0
  112. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/quickstarts/index.rst +0 -0
  113. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/readme.rst +0 -0
  114. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/references/index.rst +0 -0
  115. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/testing.rst +0 -0
  116. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/manage.py +0 -0
  117. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/pylintrc +0 -0
  118. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/pylintrc_tweaks +0 -0
  119. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/ruff.toml +0 -0
  120. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/scripts/generate_paragon_icons.py +0 -0
  121. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/setup.cfg +0 -0
  122. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/__init__.py +0 -0
  123. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/admin.py +0 -0
  124. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/__init__.py +0 -0
  125. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/catalog.py +0 -0
  126. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/data.py +0 -0
  127. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/permissions.py +0 -0
  128. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/roles.py +0 -0
  129. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/users.py +0 -0
  130. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/utils.py +0 -0
  131. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/apps.py +0 -0
  132. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/__init__.py +0 -0
  133. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/schema/course_permissions.yaml +0 -0
  134. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/schema/course_roles.yaml +0 -0
  135. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/schema/library_permissions.yaml +0 -0
  136. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/schema/library_roles.yaml +0 -0
  137. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/conf/locale/config.yaml +0 -0
  138. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/constants/__init__.py +0 -0
  139. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/constants/permissions.py +0 -0
  140. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/constants/roles.py +0 -0
  141. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/data.py +0 -0
  142. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/__init__.py +0 -0
  143. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/adapter.py +0 -0
  144. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/apps.py +0 -0
  145. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/config/authz.policy +0 -0
  146. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/config/model.conf +0 -0
  147. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/enforcer.py +0 -0
  148. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/filter.py +0 -0
  149. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/matcher.py +0 -0
  150. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/policy.py +0 -0
  151. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/renderer.py +0 -0
  152. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/__init__.py +0 -0
  153. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/compilation.py +0 -0
  154. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/discovery.py +0 -0
  155. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/exceptions.py +0 -0
  156. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/paragon_icons.py +0 -0
  157. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/translation.py +0 -0
  158. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/types/compilation.py +0 -0
  159. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/types/definitions.py +0 -0
  160. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/utils.py +0 -0
  161. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/filters.py +0 -0
  162. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/handlers.py +0 -0
  163. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/__init__.py +0 -0
  164. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/__init__.py +0 -0
  165. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/authz_migrate_course_authoring.py +0 -0
  166. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/authz_rollback_course_authoring.py +0 -0
  167. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/enforcement.py +0 -0
  168. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/extract_schema_translations.py +0 -0
  169. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/load_authz_schema.py +0 -0
  170. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/load_policies.py +0 -0
  171. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0001_add_casbin_dependency.py +0 -0
  172. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0002_initial.py +0 -0
  173. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0003_usersubject.py +0 -0
  174. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0004_contentlibraryscope.py +0 -0
  175. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0005_policycachecontrol.py +0 -0
  176. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0006_migrate_legacy_permissions.py +0 -0
  177. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0007_coursescope.py +0 -0
  178. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0008_authzcourseauthoringmigrationrun.py +0 -0
  179. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0009_roleassignmentaudit.py +0 -0
  180. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0010_scope_external_key.py +0 -0
  181. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0011_authz_schema_definitions.py +0 -0
  182. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/__init__.py +0 -0
  183. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/__init__.py +0 -0
  184. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/authz_migration.py +0 -0
  185. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/base.py +0 -0
  186. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/core.py +0 -0
  187. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/engine.py +0 -0
  188. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/schema.py +0 -0
  189. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/scopes.py +0 -0
  190. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/subjects.py +0 -0
  191. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/__init__.py +0 -0
  192. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/data.py +0 -0
  193. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/decorators.py +0 -0
  194. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/urls.py +0 -0
  195. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/utils.py +0 -0
  196. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/__init__.py +0 -0
  197. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/admin_console/__init__.py +0 -0
  198. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/admin_console/filters.py +0 -0
  199. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/admin_console/serializers.py +0 -0
  200. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/admin_console/views.py +0 -0
  201. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/course_authoring/__init__.py +0 -0
  202. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/course_authoring/pipeline.py +0 -0
  203. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/course_authoring/views.py +0 -0
  204. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/fields.py +0 -0
  205. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/paginators.py +0 -0
  206. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/permissions.py +0 -0
  207. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/serializers.py +0 -0
  208. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/urls.py +0 -0
  209. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/views.py +0 -0
  210. {openedx_authz-2.0.0/src/openedx_authz/schema → openedx_authz-2.1.0/src/openedx_authz/settings}/__init__.py +0 -0
  211. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/settings/production.py +0 -0
  212. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/settings/test.py +0 -0
  213. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/templates/openedx_authz/base.html +0 -0
  214. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/__init__.py +0 -0
  215. {openedx_authz-2.0.0/src/openedx_authz/settings → openedx_authz-2.1.0/src/openedx_authz/tests/api}/__init__.py +0 -0
  216. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/api/test_catalog.py +0 -0
  217. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/api/test_data.py +0 -0
  218. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/api/test_roles.py +0 -0
  219. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/api/test_users.py +0 -0
  220. {openedx_authz-2.0.0/src/openedx_authz/tests/api → openedx_authz-2.1.0/src/openedx_authz/tests/authz}/__init__.py +0 -0
  221. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/authz/test_resources.py +0 -0
  222. {openedx_authz-2.0.0/src/openedx_authz/tests/authz → openedx_authz-2.1.0/src/openedx_authz/tests/integration}/__init__.py +0 -0
  223. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/conftest.py +0 -0
  224. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/test_handlers.py +0 -0
  225. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/test_models.py +0 -0
  226. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/test_schema_apply.py +0 -0
  227. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/test_views.py +0 -0
  228. {openedx_authz-2.0.0/src/openedx_authz/tests/integration → openedx_authz-2.1.0/src/openedx_authz/tests/rest_api}/__init__.py +0 -0
  229. {openedx_authz-2.0.0/src/openedx_authz/tests/rest_api → openedx_authz-2.1.0/src/openedx_authz/tests/rest_api/admin_console}/__init__.py +0 -0
  230. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/admin_console/test_views.py +0 -0
  231. {openedx_authz-2.0.0/src/openedx_authz/tests/rest_api/admin_console → openedx_authz-2.1.0/src/openedx_authz/tests/rest_api/course_authoring}/__init__.py +0 -0
  232. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/course_authoring/test_pipeline.py +0 -0
  233. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/course_authoring/test_views.py +0 -0
  234. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/mixins.py +0 -0
  235. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/test_permissions.py +0 -0
  236. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/test_utils.py +0 -0
  237. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/test_views.py +0 -0
  238. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/__init__.py +0 -0
  239. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_apply.py +0 -0
  240. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_compilation.py +0 -0
  241. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_discovery.py +0 -0
  242. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_load_authz_schema_command.py +0 -0
  243. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_renderer.py +0 -0
  244. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_schema_models.py +0 -0
  245. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_source_storage.py +0 -0
  246. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_translation.py +0 -0
  247. {openedx_authz-2.0.0/src/openedx_authz/tests/rest_api/course_authoring → openedx_authz-2.1.0/src/openedx_authz/tests/stubs}/__init__.py +0 -0
  248. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/apps.py +0 -0
  249. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/migrations/0001_initial.py +0 -0
  250. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/migrations/0002_learningpackage_contentlibrary_learning_package.py +0 -0
  251. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/migrations/0002_userprofile.py +0 -0
  252. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/models.py +0 -0
  253. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_commands.py +0 -0
  254. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_enforcement.py +0 -0
  255. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_enforcer.py +0 -0
  256. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_engine_utils.py +0 -0
  257. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_filter.py +0 -0
  258. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_handlers.py +0 -0
  259. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_imports.py +0 -0
  260. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_migrations.py +0 -0
  261. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_models.py +0 -0
  262. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_policy.py +0 -0
  263. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_utils.py +0 -0
  264. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/urls.py +0 -0
  265. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/utils.py +0 -0
  266. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/dependency_links.txt +0 -0
  267. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/entry_points.txt +0 -0
  268. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/top_level.txt +0 -0
  269. {openedx_authz-2.0.0 → openedx_authz-2.1.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openedx-authz
3
- Version: 2.0.0
3
+ Version: 2.1.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
 
@@ -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": {
@@ -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
+ )
@@ -0,0 +1,169 @@
1
+ """Read discovered resources into parsed documents (the ``parse`` step, ADR 0018).
2
+
3
+ Parses each YAML schema resource without imposing authorization-schema
4
+ structure, attaching its :class:`SourceRecord` with a content digest computed
5
+ from the exact bytes read. JSON Schema validation owns structural requirements,
6
+ typed shaping belongs to :mod:`.loading`, and semantic checks belong to
7
+ :mod:`.validation`.
8
+
9
+ No Casbin or Django imports, so parsing stays unit-testable in isolation.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import logging
16
+ from importlib import metadata
17
+
18
+ import yaml
19
+
20
+ from openedx_authz.engine.schema.discovery import DiscoveredResource
21
+ from openedx_authz.engine.schema.exceptions import SchemaLoadError
22
+ from openedx_authz.engine.schema.types import ParsedSchemaDocument, SourceRecord
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ UNKNOWN_DISTRIBUTION = "unknown"
27
+
28
+
29
+ class SchemaParser:
30
+ """Read discovered resources into untyped YAML values with provenance.
31
+
32
+ Each :class:`DiscoveredResource` knows how to read itself, so the parser
33
+ controls when files are read and computes the content digest from the exact
34
+ bytes passed to the YAML parser.
35
+ """
36
+
37
+ distribution_ambiguity_warned: set[tuple[str, str, str]] = set()
38
+ """Tracks warned (package, resource_path, selected) combinations to deduplicate logs."""
39
+
40
+ def parse(self, resources: list[DiscoveredResource]) -> list[ParsedSchemaDocument]:
41
+ """Parse every resource without imposing authorization-schema structure."""
42
+ documents = []
43
+ for resource in resources:
44
+ contents = resource.read_bytes()
45
+ data = self.parse_yaml(contents, resource)
46
+ schema_version = str(data.get("schema_version", "")) if isinstance(data, dict) else ""
47
+ source = self.build_source_record(resource, contents, schema_version)
48
+ documents.append(ParsedSchemaDocument(source=source, data=data))
49
+ return documents
50
+
51
+ @staticmethod
52
+ def parse_yaml(contents: bytes, resource: DiscoveredResource) -> object:
53
+ """Parse YAML bytes and preserve the resulting root value."""
54
+ try:
55
+ return yaml.safe_load(contents)
56
+ except yaml.YAMLError as exc:
57
+ raise SchemaLoadError(f"Invalid YAML in {resource.package}:{resource.resource_path}: {exc}") from exc
58
+
59
+ def build_source_record(self, resource: DiscoveredResource, contents: bytes, schema_version: str) -> SourceRecord:
60
+ """Assemble packaging metadata and a digest into a SourceRecord.
61
+
62
+ Resolves the installed distribution name/version that owns the resource
63
+ package via ``importlib.metadata`` and hashes ``contents`` for the
64
+ digest. Falls back to ``"unknown"`` when the package is not tied to an
65
+ installed distribution (e.g. operator-supplied settings resources).
66
+ """
67
+ distribution, version = self.resolve_distribution(resource)
68
+ content_digest = hashlib.sha256(contents).hexdigest()
69
+ return SourceRecord(
70
+ distribution=distribution,
71
+ distribution_version=version,
72
+ module=resource.module,
73
+ resource_path=resource.resource_path,
74
+ schema_version=schema_version,
75
+ content_digest=content_digest,
76
+ )
77
+
78
+ @classmethod
79
+ def resolve_distribution(cls, resource: DiscoveredResource) -> tuple[str, str]:
80
+ """Map a discovered resource to its providing distribution name and version.
81
+
82
+ A top-level import package can be provided by more than one installed
83
+ distribution (namespace packages, overlapping/legacy installs), so
84
+ ``packages_distributions()`` returns a *list* of candidate names. The
85
+ same source must resolve to the same distribution under every deployment
86
+ layout (ADR 0019 §2), so an arbitrary ``candidates[0]`` is not good
87
+ enough: when several distributions claim the top-level package we pick
88
+ the one that actually ships this resource's file, and only fall back to
89
+ a deterministic (sorted) choice when no single owner can be identified.
90
+ """
91
+ top_level = resource.package.split(".", 1)[0]
92
+ try:
93
+ mapping = metadata.packages_distributions()
94
+ except Exception: # pylint: disable=broad-exception-caught # noqa: BLE001 - defensive; metadata quirks across envs
95
+ mapping = {}
96
+ # ``packages_distributions()`` can list the same distribution more than
97
+ # once for one top-level package (observed with editable installs and
98
+ # overlapping metadata). Those are not competing owners, so collapse to
99
+ # the distinct names before resolving; otherwise a lone real owner that
100
+ # happens to be listed twice looks "ambiguous" and the fallback path
101
+ # warns on every resource for a non-problem.
102
+ candidates = sorted(set(mapping.get(top_level) or []))
103
+ if not candidates:
104
+ return top_level, UNKNOWN_DISTRIBUTION
105
+
106
+ distribution = cls.select_owning_distribution(candidates, resource)
107
+ try:
108
+ return distribution, metadata.version(distribution)
109
+ except metadata.PackageNotFoundError:
110
+ return distribution, UNKNOWN_DISTRIBUTION
111
+
112
+ @classmethod
113
+ def select_owning_distribution(cls, candidates: list[str], resource: DiscoveredResource) -> str:
114
+ """Choose the distribution that ships ``resource`` from the candidate list.
115
+
116
+ With a single candidate there is nothing to disambiguate. With several,
117
+ we match each distribution's recorded file list against the resource's
118
+ installed path (``package/resource_path``) and return the one that owns
119
+ it. If exactly zero or more than one distribution claims the file (or the
120
+ file lists are unavailable), we cannot know the true owner, so we return
121
+ the first candidate in sorted order, a stable choice across environments,
122
+ and log the ambiguity once per distinct (package, resource_path, selected)
123
+ combination.
124
+ """
125
+ if len(candidates) == 1:
126
+ return candidates[0]
127
+
128
+ installed_path = f"{resource.package}/{resource.resource_path}"
129
+ owners = [name for name in candidates if cls.distribution_ships(name, installed_path)]
130
+ if len(owners) == 1:
131
+ return owners[0]
132
+
133
+ fallback = sorted(candidates)[0]
134
+ # Deduplicate warnings by tracking (package, resource_path, selected) combinations.
135
+ warn_key = (resource.package, resource.resource_path, fallback)
136
+ if warn_key not in cls.distribution_ambiguity_warned:
137
+ cls.distribution_ambiguity_warned.add(warn_key)
138
+ logger.info(
139
+ "Schema resource for package '%s' is claimed by multiple distributions %s; "
140
+ "deterministically selected '%s'.",
141
+ resource.package,
142
+ sorted(candidates),
143
+ fallback,
144
+ extra={
145
+ "package": resource.package,
146
+ "resource_path": resource.resource_path,
147
+ "candidates": sorted(candidates),
148
+ "matched_owners": sorted(owners),
149
+ "selected": fallback,
150
+ },
151
+ )
152
+ return fallback
153
+
154
+ @staticmethod
155
+ def distribution_ships(distribution: str, installed_path: str) -> bool:
156
+ """Return whether ``distribution`` records a file matching ``installed_path``.
157
+
158
+ ``Distribution.files`` lists the files the distribution installed, as
159
+ anchor-relative ``PackagePath`` values (e.g.
160
+ ``openedx_authz/authz/schema/course_roles.yaml``). A resource belongs to
161
+ the distribution when one of those paths ends with the resource's
162
+ ``package/resource_path``. Metadata gaps (``files`` is ``None`` or the
163
+ distribution is missing) mean "cannot confirm ownership", not an error.
164
+ """
165
+ try:
166
+ files = metadata.distribution(distribution).files or []
167
+ except metadata.PackageNotFoundError:
168
+ return False
169
+ return any(str(path).endswith(installed_path) for path in files)
@@ -2,7 +2,7 @@
2
2
 
3
3
  :class:`SchemaPipeline` wires the steps together:
4
4
 
5
- discover -> load -> validate -> compile -> render -> (plan) -> apply
5
+ discover -> parse -> validate structure -> load -> validate semantics -> compile -> render -> (plan) -> apply
6
6
 
7
7
  The Casbin-free steps (discover..compile) live in :mod:`openedx_authz.engine.schema`;
8
8
  render/apply live in :mod:`openedx_authz.engine.renderer`. This orchestrator is
@@ -26,9 +26,12 @@ from openedx_authz.engine.renderer import (
26
26
  from openedx_authz.engine.schema.compilation import SchemaCompiler
27
27
  from openedx_authz.engine.schema.discovery import SchemaDiscovery
28
28
  from openedx_authz.engine.schema.exceptions import SchemaValidationError
29
+ from openedx_authz.engine.schema.issues import ValidationIssue
29
30
  from openedx_authz.engine.schema.loading import SchemaLoader
31
+ from openedx_authz.engine.schema.parsing import SchemaParser
32
+ from openedx_authz.engine.schema.syntax import SchemaSyntaxValidator
30
33
  from openedx_authz.engine.schema.types import CompiledSchema
31
- from openedx_authz.engine.schema.validation import SchemaValidator, ValidationIssue
34
+ from openedx_authz.engine.schema.validation import SchemaValidator
32
35
 
33
36
  logger = logging.getLogger(__name__)
34
37
 
@@ -44,6 +47,8 @@ class SchemaPipeline:
44
47
  self,
45
48
  *,
46
49
  discovery: SchemaDiscovery | None = None,
50
+ parser: SchemaParser | None = None,
51
+ syntax_validator: SchemaSyntaxValidator | None = None,
47
52
  loader: SchemaLoader | None = None,
48
53
  validator: SchemaValidator | None = None,
49
54
  compiler: SchemaCompiler | None = None,
@@ -51,6 +56,8 @@ class SchemaPipeline:
51
56
  applier: SchemaApplier | None = None,
52
57
  ):
53
58
  self._discovery = discovery or SchemaDiscovery()
59
+ self._parser = parser or SchemaParser()
60
+ self._syntax_validator = syntax_validator or SchemaSyntaxValidator()
54
61
  self._loader = loader or SchemaLoader()
55
62
  self._validator = validator or SchemaValidator()
56
63
  self._compiler = compiler or SchemaCompiler()
@@ -58,7 +65,7 @@ class SchemaPipeline:
58
65
  self._applier = applier or SchemaApplier()
59
66
 
60
67
  def compile(self) -> CompiledSchema:
61
- """Run discover -> load -> validate -> compile and return the result.
68
+ """Run discover -> parse -> validate syntax -> load -> validate semantics -> compile and return the result.
62
69
 
63
70
  Validation gates twice: once on the loaded documents, then again on the
64
71
  compiled schema, because extensions and priority resolution can only be
@@ -70,7 +77,9 @@ class SchemaPipeline:
70
77
  SchemaCompileError: On an unresolvable conflict.
71
78
  """
72
79
  resources = self._discovery.discover()
73
- documents = self._loader.load(resources)
80
+ parsed_documents = self._parser.parse(resources)
81
+ self._gate(self._syntax_validator.validate(parsed_documents))
82
+ documents = self._loader.load(parsed_documents)
74
83
 
75
84
  self._gate(self._validator.validate(documents))
76
85
  schema = self._compiler.compile(documents)
@@ -87,7 +96,7 @@ class SchemaPipeline:
87
96
  for issue in issues:
88
97
  log = logger.error if issue.is_error else logger.warning
89
98
  log("authz schema %s: %s [%s]", issue.level, issue.message, issue.source_id or "-")
90
- if self._validator.has_errors(issues):
99
+ if any(issue.is_error for issue in issues):
91
100
  raise SchemaValidationError([i for i in issues if i.is_error])
92
101
 
93
102
  def plan(self) -> ChangePlan: