From fb5bcf1a5509134befa94f4a6da507b62766bce4 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 1 Sep 2026 18:10:48 +0200 Subject: [PATCH 1/6] docs: add authorization schema reference --- .../0017-static-authorization-schema.rst | 2 + docs/decisions/0023-extend-static-roles.rst | 3 +- docs/references/authorization-schema.rst | 347 ++++++++++++++++++ docs/references/index.rst | 5 + 4 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 docs/references/authorization-schema.rst diff --git a/docs/decisions/0017-static-authorization-schema.rst b/docs/decisions/0017-static-authorization-schema.rst index 4e271175..2ad4850e 100644 --- a/docs/decisions/0017-static-authorization-schema.rst +++ b/docs/decisions/0017-static-authorization-schema.rst @@ -23,6 +23,8 @@ The authz schema is a versioned YAML format for static permissions, permission c 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. +The :ref:`Authorization Schema Reference` describes every field, the naming conventions, and complete configuration examples. + 2. Permissions and categories ============================= diff --git a/docs/decisions/0023-extend-static-roles.rst b/docs/decisions/0023-extend-static-roles.rst index 8482ae65..c62bf9f9 100644 --- a/docs/decisions/0023-extend-static-roles.rst +++ b/docs/decisions/0023-extend-static-roles.rst @@ -19,7 +19,7 @@ Decision 1. Role extension fields ======================== -A ``role_extensions`` entry identifies an existing static role with ``role`` and changes only the fields included in the entry. It may use: +A ``role_extensions`` entry identifies an existing static role with ``role`` and changes only the fields included in the entry. The :ref:`Authorization Schema Reference` describes these fields and includes complete examples for applications and Tutor configuration. An entry may use: * ``add_permissions`` to add complete permission IDs; * ``remove_permissions`` to remove complete permission IDs; @@ -131,6 +131,7 @@ References * `ADR 0017`_ * `ADR 0018`_ * `ADR 0019`_ +* :ref:`Authorization Schema Reference` * `Tutor plugin development`_ .. _ADR 0017: 0017-static-authorization-schema.rst diff --git a/docs/references/authorization-schema.rst b/docs/references/authorization-schema.rst new file mode 100644 index 00000000..620a965e --- /dev/null +++ b/docs/references/authorization-schema.rst @@ -0,0 +1,347 @@ +.. _Authorization Schema Reference: + +Authorization Schema Reference +############################## + +The Open edX Authorization schema, or authz schema, is a YAML configuration format for static permissions, permission categories, roles, and changes to existing roles. Applications ship schema files with their code, while site operators can contribute the same format through their deployment configuration. Deployment validates and compiles all contributions into the policy used by ``openedx-authz``. + +Use this reference when creating or reviewing an authz schema file. The examples omit fields only when the surrounding section does not need them. + +.. contents:: Contents + :depth: 2 + :local: + +Complete example +**************** + +The following file defines one category, two permissions, one role, and an extension to a role defined elsewhere: + +.. code-block:: yaml + + schema_version: "1.0" + priority: 100 + + permission_categories: + - id: course_content + display_name: Course content + description: Permissions for viewing and editing course content. + icon: Article + + permissions: + - namespace: courses + name: view_course + display_name: View course + description: View course configuration and content. + category: course_content + scopes: + - course-v1 + icon: Visibility + + - namespace: courses + name: view_course_updates + display_name: View course updates + description: View course update posts. + category: course_content + scopes: + - course-v1 + icon: Visibility + + roles: + - id: course_observer + display_name: Course observer + description: Reviews a course without changing it. + scopes: + - course-v1 + permissions: + - courses.view_course + - courses.view_course_updates + + role_extensions: + - role: course_editor + add_permissions: + - courses.export_course + +Top-level fields +**************** + +``schema_version`` +================== + +The version of the YAML format used by the file. Write it as a quoted ``major.minor`` value, such as ``"1.0"``. A deployment stops before changing the database when it encounters a version it cannot read. + +``priority`` +============ + +An integer used when several files change the same definition or role field. A higher number takes precedence. Contributions with the same priority may be combined when they agree or affect different fields, but conflicting values at the same priority fail validation. + +Priority does not control the order shown in a user interface. Clients may sort roles, permissions, and categories for their own presentation. + +``permission_categories`` +========================= + +A list of category definitions used to group permissions for display and discovery. Categories do not grant access. + +``permissions`` +=============== + +A list of permission definitions. Application checks use the stable permission ID formed from each permission's ``namespace`` and ``name``. + +``roles`` +========= + +A list of static role definitions. A role lists every permission assigned to it. + +``role_extensions`` +=================== + +A list of changes to static roles defined in this file or another schema contribution. An extension changes only the fields it includes and does not copy or replace the complete role. + +Permission categories +********************* + +A category contains these fields: + +``id`` + The stable category identifier. It is required and uses lowercase snake case, such as ``course_content`` or ``library_management``. Category IDs are global and do not include a permission namespace. Applications that use the same ID contribute permissions to the same category. + +``display_name`` + The source-language name shown to users. It uses sentence case and is translated through the authz schema translation process. + +``description`` + A complete source-language sentence describing the group of permissions. + +``icon`` + An optional icon name exported by ``@openedx/paragon/icons``. The value is case-sensitive, such as ``Article``. + +For example: + +.. code-block:: yaml + + permission_categories: + - id: library_management + display_name: Library management + description: Permissions for managing content libraries. + icon: Article + +Permissions +*********** + +A permission contains these fields: + +``namespace`` + The stable product domain that owns the permission. It uses lowercase snake case, such as ``courses`` or ``content_libraries``. The namespace does not need to match the Python package, Django app, IDA, or Tutor plugin that contributes the file. Code may move between applications without changing the permission ID. + +``name`` + The operation within the product domain. It uses lowercase snake case and normally begins with a verb, such as ``view_course``, ``export_course``, or ``manage_library_tags``. + +``display_name`` + The source-language name shown to users. Changing it does not change the permission ID used by application checks. + +``description`` + A complete source-language sentence describing the access controlled by the permission. + +``category`` + The complete ID of a category defined in the combined schema. + +``scopes`` + The scope namespaces where the permission can apply. These values come from registered ``ScopeData`` types, such as ``course-v1``, ``ccx-v1``, or ``lib``. + +``icon`` + An optional, case-sensitive icon name exported by ``@openedx/paragon/icons``. + +The complete permission ID joins ``namespace`` and ``name`` with a period. For example: + +.. code-block:: yaml + + permissions: + - namespace: content_libraries + name: manage_library_tags + display_name: Manage library tags + description: Add, edit, and remove tags in a content library. + category: library_management + scopes: + - lib + +The complete ID is ``content_libraries.manage_library_tags``. Role definitions, role extensions, application checks, and API responses use this value. + +The Casbin form ``act^content_libraries.manage_library_tags`` is an internal value and is not valid in a schema file. + +Roles +***** + +A role contains these fields: + +``id`` + The stable role identifier. It uses lowercase snake case, such as ``course_admin``, ``course_editor``, or ``library_author``. Role IDs do not include a product namespace because authorization uses the role within its supported scopes. + +``display_name`` + The source-language name shown to users. + +``description`` + A complete source-language sentence describing what the role can do. + +``scopes`` + The scope namespaces where the role can be assigned. Every permission listed by the role must support those scopes. + +``permissions`` + A list of complete permission IDs. The compiler does not infer one permission from another, so the role lists every permission it needs. + +``icon`` + An optional, case-sensitive icon name exported by ``@openedx/paragon/icons``. + +``hidden`` + An optional boolean that defaults to ``false``. A hidden role does not appear in normal role discovery and selection interfaces. Hiding does not delete the role, remove existing assignments, or change permission checks. + +For example: + +.. code-block:: yaml + + roles: + - id: library_reviewer + display_name: Library reviewer + description: Reviews library content without publishing it. + scopes: + - lib + permissions: + - content_libraries.view_library + - content_libraries.view_library_team + icon: Visibility + +The Casbin form ``role^library_reviewer`` is an internal value and is not valid as ``roles.id`` or in a ``role_extensions.role`` reference. + +Role extensions +*************** + +A role extension contains ``role`` and at least one field to change: + +``role`` + The complete ID of an existing static role. + +``add_permissions`` + Complete permission IDs to add to the role. + +``remove_permissions`` + Complete permission IDs to remove from the role. + +``display_name``, ``description``, and ``icon`` + Display metadata to replace. Metadata fields left out of the extension keep their current values. + +``hidden`` + Whether the role appears in normal role discovery and selection interfaces. + +For example, a deployment can allow course editors to export courses, remove their access to tag management, change the displayed role name, and hide the course auditor role: + +.. code-block:: yaml + + schema_version: "1.0" + priority: 200 + + role_extensions: + - role: course_editor + add_permissions: + - courses.export_course + remove_permissions: + - courses.manage_tags + display_name: Course author + description: Creates and exports course content. + + - role: course_auditor + hidden: true + +An extension fails validation when its target role or a referenced permission does not exist. Adding a permission already assigned to the role or removing one the role does not have produces a warning and leaves the result unchanged. + +Identifier rules +**************** + +Permission namespaces, permission names, category IDs, and role IDs use lowercase letters, numbers, and underscores, begin with a letter, and match ``[a-z][a-z0-9_]*``. The period in a complete permission ID separates its namespace from its name and does not appear inside either part. + +Valid identifiers include: + +.. code-block:: text + + courses + view_course + courses.view_course + course_content + course_editor + +The following values are invalid: + +.. code-block:: text + + Courses.view_course # uppercase letter + courses:view_course # wrong separator + act^courses.view_course # internal Casbin namespace + course content # space + role^course_editor # internal Casbin namespace + +Scope namespaces follow the spelling registered by their ``ScopeData`` type and may contain a hyphen. Do not apply the snake-case identifier rule to values such as ``course-v1`` or ``ccx-v1``. + +Schema files in applications +**************************** + +Applications keep schema resources under an ``authz`` package directory and use the ``.authz.yaml`` suffix. The filename describes the definitions in the file using lowercase snake case: + +.. code-block:: text + + course_authoring/ + └── authz/ + ├── course_permissions.authz.yaml + └── course_roles.authz.yaml + +The application exposes these package resources through the ``openedx-authz`` schema entry point. Resource paths are relative to the Python module, which keeps discovery independent of virtual-environment and container paths. + +Tutor configuration for site operators +************************************** + +A site operator can provide an authz schema through the ``openedx-authz-schema`` patch. Run ``tutor plugins printroot`` to find the local plugin directory, then create ``openedx-authz-overrides.yml`` there: + +.. code-block:: yaml + + name: openedx-authz-overrides + version: 0.1.0 + + patches: + openedx-authz-schema: | + schema_version: "1.0" + priority: 200 + + role_extensions: + - role: course_editor + add_permissions: + - courses.export_course + remove_permissions: + - courses.manage_tags + display_name: Course author + description: Creates and exports course content. + + - role: course_auditor + hidden: true + +Enable the plugin and save the rendered Tutor configuration: + +.. code-block:: console + + tutor plugins enable openedx-authz-overrides + tutor config save + +The next deployment validates and compiles the patch with the schema files provided by applications. + +Checking the resulting permissions +********************************** + +After deployment, use the existing ``enforcement`` management command to check the policy stored in the database: + +.. code-block:: console + + tutor local run lms ./manage.py lms enforcement + +The command expects a subject, complete permission ID, and scope. Assuming ``alice`` has ``course_editor`` in ``course-v1:OpenedX+DemoX+DemoCourse``, the extension above produces these results: + +.. code-block:: text + + alice courses.export_course course-v1:OpenedX+DemoX+DemoCourse + ✓ ALLOWED: alice courses.export_course course-v1:OpenedX+DemoX+DemoCourse + + alice courses.manage_tags course-v1:OpenedX+DemoX+DemoCourse + ✗ DENIED: alice courses.manage_tags course-v1:OpenedX+DemoX+DemoCourse diff --git a/docs/references/index.rst b/docs/references/index.rst index ba5ea57c..204ae634 100644 --- a/docs/references/index.rst +++ b/docs/references/index.rst @@ -1,2 +1,7 @@ References ########## + +.. toctree:: + :maxdepth: 1 + + authorization-schema From ed0c628071fa2b3240264bb8be47ce4111c7a1b8 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 2 Sep 2026 19:08:32 +0200 Subject: [PATCH 2/6] docs: show application and Tutor schema discovery --- .../0019-authorization-schema-discovery.rst | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/decisions/0019-authorization-schema-discovery.rst b/docs/decisions/0019-authorization-schema-discovery.rst index 6728150f..6ecf2ec3 100644 --- a/docs/decisions/0019-authorization-schema-discovery.rst +++ b/docs/decisions/0019-authorization-schema-discovery.rst @@ -31,32 +31,29 @@ The registered function returns the package resources that contain its schema: .. code-block:: python - ... def get_schema_resources(): return ["authz/course_authoring.authz.yaml"] -The compiler loads the ``authz.schema`` entry-point group, calls each registered function, and uses ``importlib.resources`` or a similar mechanism to find the files. It then validates the schemas, compiles them into Casbin rows, and applies them to the database. +The compiler loads the ``authz.schema`` entry-point group, calls each registered function, and resolves the returned paths with ``importlib.resources``. If a registered function raises an exception, discovery stops and reports which application failed. This prevents deployment from continuing with an incomplete set of static definitions. -Site operators can contribute a schema through the ``openedx-authz-schema`` Tutor patch: +Site operators can use a Tutor patch to add a schema that belongs to their deployment: -.. code-block:: yaml - - name: openedx-authz-overrides - version: 0.1.0 +.. code-block:: python - patches: - openedx-authz-schema: | - schema_version: "1.0" - priority: 200 + from tutor import hooks - role_extensions: - - role: course_editor - add_permissions: - - courses.export_course + hooks.Filters.ENV_PATCHES.add_item(( + "openedx-common-settings", + """ + OPENEDX_AUTHZ_SCHEMA_RESOURCES += [ + ("site_authorization", "authz/site.authz.yaml"), + ] + """, + )) -The compiler combines schemas from application entry points and Tutor patches in a defined order because discovery order may vary. +Both options pass package names and resource paths to the same compiler. The compiler returns all contributions in a defined order because package discovery order may vary. 2. Static source information ============================ From ba31f9b649bdf7dec17e142d01f87f05f621a588 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 2 Sep 2026 19:13:55 +0200 Subject: [PATCH 3/6] docs: keep discovery guidance in its owning PR --- .../0019-authorization-schema-discovery.rst | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/decisions/0019-authorization-schema-discovery.rst b/docs/decisions/0019-authorization-schema-discovery.rst index 6ecf2ec3..8203851a 100644 --- a/docs/decisions/0019-authorization-schema-discovery.rst +++ b/docs/decisions/0019-authorization-schema-discovery.rst @@ -17,17 +17,16 @@ Decision 1. Python entry point and package resources =========================================== -Applications can register schema resources through a Python entry point defined by ``openedx-authz``, following the pattern used to register LMS and CMS Django apps. For example, the ``course_authoring`` package can add an ``authz.schema`` entry point in ``setup.py``: +An application contributes one or more authz schema resources through a Python entry point defined by ``openedx-authz``. Discovery resolves those resources with the available mechanisms (like we discover Django applications or using ``importlib.resources``) and returns all contributions in a defined order, since package discovery order may vary. -.. code-block:: python +For example, the ``course_authoring`` package can register its schema function in ``pyproject.toml``: + +.. code-block:: toml - entry_points={ - "authz.schema": [ - "course_authoring = course_authoring.authz:get_schema_resources", - ], - } + [project.entry-points."authz.schema"] + course_authoring = "course_authoring.authz:get_schema_resources" -The registered function returns the package resources that contain its schema: +The function returns the schema resources provided by the package: .. code-block:: python From 1bfc255f39de88f8b7491af751c81e9f2ca499fc Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 2 Sep 2026 19:15:33 +0200 Subject: [PATCH 4/6] docs: retain discovery guidance from its owning PR --- .../0019-authorization-schema-discovery.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/decisions/0019-authorization-schema-discovery.rst b/docs/decisions/0019-authorization-schema-discovery.rst index 8203851a..062afb92 100644 --- a/docs/decisions/0019-authorization-schema-discovery.rst +++ b/docs/decisions/0019-authorization-schema-discovery.rst @@ -17,19 +17,21 @@ Decision 1. Python entry point and package resources =========================================== -An application contributes one or more authz schema resources through a Python entry point defined by ``openedx-authz``. Discovery resolves those resources with the available mechanisms (like we discover Django applications or using ``importlib.resources``) and returns all contributions in a defined order, since package discovery order may vary. +Applications can register schema resources through a Python entry point defined by ``openedx-authz``, following the pattern used to register LMS and CMS Django apps. For example, the ``course_authoring`` package can add an ``authz.schema`` entry point in ``setup.py``: -For example, the ``course_authoring`` package can register its schema function in ``pyproject.toml``: - -.. code-block:: toml +.. code-block:: python - [project.entry-points."authz.schema"] - course_authoring = "course_authoring.authz:get_schema_resources" + entry_points={ + "authz.schema": [ + "course_authoring = course_authoring.authz:get_schema_resources", + ], + } -The function returns the schema resources provided by the package: +The registered function returns the package resources that contain its schema: .. code-block:: python + ... def get_schema_resources(): return ["authz/course_authoring.authz.yaml"] From bafbf52cac33910bd15482fa96cbde0b6b76a2a2 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Fri, 4 Sep 2026 14:06:16 +0200 Subject: [PATCH 5/6] docs: align reference priority behavior --- docs/references/authorization-schema.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/authorization-schema.rst b/docs/references/authorization-schema.rst index 620a965e..c14e12e3 100644 --- a/docs/references/authorization-schema.rst +++ b/docs/references/authorization-schema.rst @@ -72,7 +72,7 @@ The version of the YAML format used by the file. Write it as a quoted ``major.mi ``priority`` ============ -An integer used when several files change the same definition or role field. A higher number takes precedence. Contributions with the same priority may be combined when they agree or affect different fields, but conflicting values at the same priority fail validation. +An integer used when several files extend the same role and change the same field or permission. A higher number takes precedence. Contributions with the same priority may be combined when they agree or affect different fields, but conflicting values at the same priority fail validation. Priority does not control the order shown in a user interface. Clients may sort roles, permissions, and categories for their own presentation. From 6d61fb859005c96302ba6fdbf8aeb042e065e139 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Fri, 4 Sep 2026 14:22:05 +0200 Subject: [PATCH 6/6] docs: link lifecycle naming conventions --- docs/decisions/0018-authorization-schema-lifecycle.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/0018-authorization-schema-lifecycle.rst b/docs/decisions/0018-authorization-schema-lifecycle.rst index 1b2f460b..fec1d5c4 100644 --- a/docs/decisions/0018-authorization-schema-lifecycle.rst +++ b/docs/decisions/0018-authorization-schema-lifecycle.rst @@ -59,7 +59,7 @@ For example, the loader may update the static row that links ``courses.view_cour Static and dynamic roles share the same set of names, so neither kind can reuse a name that already exists. The dynamic role API rejects a name used by a static role, and deployment stops when a new static role conflicts with an existing dynamic role. -Because this ADR covers the lifecycle of static definitions, it establishes that static role IDs follow the authz schema's naming conventions. Naming conventions for dynamic roles are outside its scope. +Because this ADR covers the lifecycle of static definitions, it establishes that static role IDs follow the conventions in the :ref:`Authorization Schema Reference`. Naming conventions for dynamic roles are outside its scope. For example, an administrator cannot create a dynamic ``course_observer`` role when an application already defines a static role with that name. If the dynamic role existed first, a deployment that introduces the static role stops and reports both the contributing package and the conflicting database record, leaving both definitions unchanged.