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