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.
- {openedx_authz-2.0.0/src/openedx_authz.egg-info → openedx_authz-2.1.0}/PKG-INFO +2 -1
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0017-static-authorization-schema.rst +7 -1
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0018-authorization-schema-lifecycle.rst +19 -0
- openedx_authz-2.1.0/docs/references/authorization-schema-json.rst +118 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/references/authorization-schema.rst +1 -1
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/pyproject.toml +1 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/constants/schema.py +3 -0
- {openedx_authz-2.0.0/src/openedx_authz → openedx_authz-2.1.0/src/openedx_authz/engine}/schema/authz-schema-v1.json +3 -3
- openedx_authz-2.1.0/src/openedx_authz/engine/schema/issues.py +37 -0
- openedx_authz-2.1.0/src/openedx_authz/engine/schema/loading.py +138 -0
- openedx_authz-2.1.0/src/openedx_authz/engine/schema/parsing.py +169 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/pipeline.py +14 -5
- openedx_authz-2.1.0/src/openedx_authz/engine/schema/syntax.py +58 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/types/__init__.py +2 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/types/loading.py +8 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/validation.py +7 -161
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/settings/common.py +4 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/categories.yaml +6 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/extensions.yaml +5 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/permissions.yaml +15 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_compiled/roles.yaml +8 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_parse/malformed.yaml +2 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_semantics/unknown_category.yaml +9 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_structure/items.yml +4 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/invalid_structure/priority.yaml +4 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/categories.yaml +6 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/extensions.yaml +5 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/permissions.yml +15 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/fixtures/schema_pipeline/valid/roles.yaml +8 -0
- openedx_authz-2.1.0/src/openedx_authz/tests/integration/test_schema_pipeline.py +117 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/factories.py +6 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_loading.py +31 -35
- openedx_authz-2.1.0/src/openedx_authz/tests/schema/test_parsing.py +51 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_pipeline.py +60 -11
- openedx_authz-2.1.0/src/openedx_authz/tests/schema/test_syntax.py +225 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_types.py +12 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_validation.py +8 -146
- {openedx_authz-2.0.0 → openedx_authz-2.1.0/src/openedx_authz.egg-info}/PKG-INFO +2 -1
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/SOURCES.txt +19 -2
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/requires.txt +1 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/scm_file_list.json +19 -2
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/uv.lock +2 -0
- openedx_authz-2.0.0/docs/references/authorization-schema-json.rst +0 -118
- openedx_authz-2.0.0/src/openedx_authz/engine/schema/loading.py +0 -287
- openedx_authz-2.0.0/src/openedx_authz/tests/stubs/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.annotation_safe_list.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.editorconfig +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/dependabot.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/add-depr-ticket-to-depr-board.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/add-remove-label-on-comment.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/ci.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/commitlint.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/release.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/self-assign-issue.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.github/workflows/upgrade-python-requirements.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.gitignore +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.pii_annotations.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/.readthedocs.yaml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/CHANGELOG.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/LICENSE +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/MANIFEST.in +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/Makefile +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/README.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/catalog-info.yaml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/codecov.yml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/Makefile +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/_images/architecture-overview.png +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/_static/theme_overrides.css +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/changelog.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/concepts/core_roles_and_permissions/content_library_roles.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/concepts/core_roles_and_permissions/course_roles.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/concepts/core_roles_and_permissions/index.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/concepts/index.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/conf.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0001-purpose-of-this-repo.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0002-authorization-model-foundation.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0003-jwt-usage.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0004-technology-selection.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0005-architecture-and-data-modeling.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0007-enforcement-mechanisms-mfe.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0008-compatibility-scheme.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0009-authz-for-course-authoring.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0010-course-authoring-flag.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0011-course-authoring-migration-process.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0012-auditability.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0012-glob-support-for-role-assignments.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0013-course-authoring-automatic-migration.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0014-bulk-assignment-queries-without-casbin-enforce.rst +0 -0
- {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
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0016-static-and-dynamic-roles.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0019-authorization-schema-discovery.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0020-authorization-schema-internationalization.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0021-authorization-definition-api.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0022-defer-authorization-implications.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0023-extend-static-roles.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0024-api-contract-for-user-grouped-role-assignments.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0025-authorization-schema-source-tracking.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0026-rest-api-domain-ownership-boundary.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0027-cross-domain-filtering-via-openedx-filters.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0028-api-contract-for-role-catalog.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0029-paragon-icon-list-maintenance.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0030-tutor-implementation-authz-schema-discovery.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0031-priority-applies-to-role-extensions-only.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/README.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/getting_started.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/how-tos/index.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/index.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/internationalization.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/make.bat +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/quickstarts/index.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/readme.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/references/index.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/testing.rst +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/manage.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/pylintrc +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/pylintrc_tweaks +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/ruff.toml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/scripts/generate_paragon_icons.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/setup.cfg +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/admin.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/catalog.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/data.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/permissions.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/roles.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/users.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/api/utils.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/apps.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/schema/course_permissions.yaml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/schema/course_roles.yaml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/schema/library_permissions.yaml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/authz/schema/library_roles.yaml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/conf/locale/config.yaml +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/constants/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/constants/permissions.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/constants/roles.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/data.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/adapter.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/apps.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/config/authz.policy +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/config/model.conf +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/enforcer.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/filter.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/matcher.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/policy.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/renderer.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/compilation.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/discovery.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/exceptions.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/paragon_icons.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/translation.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/types/compilation.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/schema/types/definitions.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/engine/utils.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/filters.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/handlers.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/authz_migrate_course_authoring.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/authz_rollback_course_authoring.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/enforcement.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/extract_schema_translations.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/load_authz_schema.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/management/commands/load_policies.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0001_add_casbin_dependency.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0002_initial.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0003_usersubject.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0004_contentlibraryscope.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0005_policycachecontrol.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0006_migrate_legacy_permissions.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0007_coursescope.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0008_authzcourseauthoringmigrationrun.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0009_roleassignmentaudit.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0010_scope_external_key.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/0011_authz_schema_definitions.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/migrations/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/authz_migration.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/base.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/core.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/engine.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/schema.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/scopes.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/models/subjects.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/data.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/decorators.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/urls.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/utils.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/admin_console/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/admin_console/filters.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/admin_console/serializers.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/admin_console/views.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/course_authoring/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/course_authoring/pipeline.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/course_authoring/views.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/fields.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/paginators.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/permissions.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/serializers.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/urls.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/rest_api/v1/views.py +0 -0
- {openedx_authz-2.0.0/src/openedx_authz/schema → openedx_authz-2.1.0/src/openedx_authz/settings}/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/settings/production.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/settings/test.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/templates/openedx_authz/base.html +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/__init__.py +0 -0
- {openedx_authz-2.0.0/src/openedx_authz/settings → openedx_authz-2.1.0/src/openedx_authz/tests/api}/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/api/test_catalog.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/api/test_data.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/api/test_roles.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/api/test_users.py +0 -0
- {openedx_authz-2.0.0/src/openedx_authz/tests/api → openedx_authz-2.1.0/src/openedx_authz/tests/authz}/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/authz/test_resources.py +0 -0
- {openedx_authz-2.0.0/src/openedx_authz/tests/authz → openedx_authz-2.1.0/src/openedx_authz/tests/integration}/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/conftest.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/test_handlers.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/test_models.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/test_schema_apply.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/integration/test_views.py +0 -0
- {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
- {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
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/admin_console/test_views.py +0 -0
- {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
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/course_authoring/test_pipeline.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/course_authoring/test_views.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/mixins.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/test_permissions.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/test_utils.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/rest_api/test_views.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/__init__.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_apply.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_compilation.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_discovery.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_load_authz_schema_command.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_renderer.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_schema_models.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_source_storage.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/schema/test_translation.py +0 -0
- {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
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/apps.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/migrations/0001_initial.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/migrations/0002_learningpackage_contentlibrary_learning_package.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/migrations/0002_userprofile.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/stubs/models.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_commands.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_enforcement.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_enforcer.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_engine_utils.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_filter.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_handlers.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_imports.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_migrations.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_models.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_policy.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/tests/test_utils.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/urls.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz/utils.py +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/dependency_links.txt +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/entry_points.txt +0 -0
- {openedx_authz-2.0.0 → openedx_authz-2.1.0}/src/openedx_authz.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
{openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0017-static-authorization-schema.rst
RENAMED
|
@@ -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
|
|
{openedx_authz-2.0.0 → openedx_authz-2.1.0}/docs/decisions/0018-authorization-schema-lifecycle.rst
RENAMED
|
@@ -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
|
|
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|