From ff64caed6bd9577f3811aa148430f073d26af2b5 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Thu, 27 Aug 2026 17:17:40 +0200 Subject: [PATCH 1/5] docs: define the static authorization schema --- .../0017-static-authorization-schema.rst | 160 ++++++++ docs/references/authorization-schema.rst | 347 ++++++++++++++++++ docs/references/index.rst | 5 + 3 files changed, 512 insertions(+) create mode 100644 docs/decisions/0017-static-authorization-schema.rst 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 new file mode 100644 index 00000000..d4f7f962 --- /dev/null +++ b/docs/decisions/0017-static-authorization-schema.rst @@ -0,0 +1,160 @@ +0017: Define Static Roles and Permissions in an Authorization Schema +#################################################################### + +Status +****** + +**Draft** + +Context +******* + +`ADR 0016`_ defines static roles in the authz schema and dynamic roles in the authz model, which the API manages through the Casbin adapter. Today, however, static roles and permissions are spread across Python constants, Python role mappings, ``authz.policy``, and frontend code. A developer who adds a role or permission must therefore update several representations, including a separate copy of its display information in the frontend. + +Casbin remains the authorization engine, and its adapter continues to read and write policy rows. The authz schema adds the Open edX format that Casbin does not provide, allowing an application to declare stable identifiers, supported scopes, role permissions, and display information in one place. During deployment, the schema is compiled into Casbin rows, while the API makes the display information available to the frontend. + +Decision +******** + +#. Schema format and boundary +============================= + +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``. Open edX publishes a YAML Schema for this format so that editors, CI, and the compiler all apply the same field and validation rules. + +The :ref:`Authorization Schema Reference` describes every field, the naming conventions, and complete configuration examples. + +#. Permissions and categories +============================= + +A permission contains: + +* ``namespace`` and ``name``, which form the stable identifier used by application checks, such as ``courses.view_course``; +* the scope namespaces where it can apply; +* a normalized permission category; and +* ``display_name``, ``description``, and an optional Paragon icon name. + +For example: + +.. code-block:: yaml + + 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: delete_course + display_name: Delete course + description: Delete a course. + category: course_content + scopes: [course-v1] + icon: Delete + +Here, ``course_content`` groups the two permissions for display. The complete permission IDs are ``courses.view_course`` and ``courses.delete_course``, while ``course-v1`` is the scope namespace where they apply. Application code uses the complete permission ID, so changing ``display_name`` does not change permission checks. + +#. Roles and role extensions +============================ + +A role contains a stable identifier, display name, description, supported scope namespaces, and a list of complete permission identifiers. When an application needs to change an existing role, it uses ``role_extensions``. An extension may add or remove permissions and may replace the role's display name or description. + +Version 1 does not define display order for roles, permissions, or categories. Clients may sort them alphabetically or apply another order that suits their interface. + +.. code-block:: yaml + + schema_version: "1.0" + priority: 100 + + roles: + - id: course_observer + display_name: Course observer + description: Can review a course without changing it. + scopes: [course-v1] + permissions: + - courses.view_course + + role_extensions: + - role: course_admin + add_permissions: + - courses.delete_course + +In these examples, ``courses.view_course`` appears under ``course_content`` in the UI and belongs to the new ``course_observer`` role. The ``role_extensions`` entry adds ``courses.delete_course`` to the existing ``course_admin`` role. The compiler can render the observer's role-permission relationship as: + +.. code-block:: text + + p, role^course_observer, act^courses.view_course, course-v1^*, allow + +The compiler also creates a row that links ``course_admin`` to ``courses.delete_course``. Priority resolves conflicts between definitions and role extensions; it does not control how roles or permissions appear in the UI. + +The schema contains static definitions, while user assignments and user-defined roles stay in the application database. Casbin's ``model.conf`` and matcher also remain owned by ``openedx-authz``. + +#. Validation and conflicts +=========================== + +Validation first checks each file against the published schema. It rejects: + +* invalid YAML, unknown fields, missing required fields, and values with the wrong type; +* unsupported schema versions; +* IDs that contain uppercase letters or unsupported punctuation; +* icon names that are unavailable from ``@openedx/paragon/icons``; and +* display fields that exceed the agreed size limits. + +After loading every file, validation checks the combined definitions. It rejects: + +* references to permissions or categories that do not exist; +* a role used in a scope where one of its permissions cannot apply; +* conflicting definitions or role extensions with the same priority; and +* unsupported combinations of schema versions. + +The validator warns about duplicate definitions, including identical definitions, and about definitions or role extensions that do not take effect because another file has a higher priority. The highest-priority contribution resolves a conflict. If several files add the same permission to the same role, the compiler creates one Casbin row and records each contributing source. + +Unit tests can load schema fixtures into an in-memory Casbin enforcer and check allowed and denied requests. This follows the model-testing approach used by OpenFGA and the schema assertions used by SpiceDB, while keeping the tests in normal application test suites. + +Consequences +************ + +* Applications declare static permission identifiers, role-to-permission assignments, and UI fields in the same YAML format. +* Application checks continue to use stable permission identifiers and do not depend on role names. +* Application clients can read the compiled definitions from the API and remove its copy. +* Schema validation needs both per-file checks and checks across all files, including conflicts and role extensions. +* Role extensions can change the permissions assigned to built-in roles. Deployment must report them clearly, and the operator remains responsible for approving them. +* Version 1 excludes permission implication and role inheritance. + +Rejected Alternatives +********************* + +Raw Casbin policy files +======================= + +They express the rows Casbin needs for permission checks but omit the display fields and source information required by users and applications. + +Python constants and role mappings +================================== + +Definitions would remain split across backend and frontend code. Applications would also need to change ``openedx-authz`` to add definitions they own. + +Embedded categories on every permission +======================================= + +Repeating category labels makes localization and consistent presentation harder. A normalized category gives permissions one stable grouping reference. + +References +********** + +* `ADR 0016`_ +* `Casbin adapters`_ +* `ASDF YAML Schema`_ +* `Paragon icons`_ + +.. _ADR 0016: 0016-static-and-dynamic-roles.rst +.. _Casbin adapters: https://v3.casbin.org/docs/adapters +.. _ASDF YAML Schema: https://www.asdf-format.org/projects/asdf-standard/en/1.0.2/schemas/yaml_schema.html +.. _Paragon icons: https://paragon-openedx.netlify.app/components/icon/ 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 78a973f811a1c16a15ef263a9f316336b2f3669a Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 1 Sep 2026 18:07:58 +0200 Subject: [PATCH 2/5] docs: move authorization schema reference to its own PR --- .../0017-static-authorization-schema.rst | 2 - docs/references/authorization-schema.rst | 347 ------------------ docs/references/index.rst | 2 - 3 files changed, 351 deletions(-) delete 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 d4f7f962..ca94ad24 100644 --- a/docs/decisions/0017-static-authorization-schema.rst +++ b/docs/decisions/0017-static-authorization-schema.rst @@ -21,8 +21,6 @@ Decision 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``. Open edX publishes a YAML Schema for this format so that editors, CI, and the compiler all apply the same field and validation rules. -The :ref:`Authorization Schema Reference` describes every field, the naming conventions, and complete configuration examples. - #. Permissions and categories ============================= diff --git a/docs/references/authorization-schema.rst b/docs/references/authorization-schema.rst deleted file mode 100644 index 620a965e..00000000 --- a/docs/references/authorization-schema.rst +++ /dev/null @@ -1,347 +0,0 @@ -.. _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 204ae634..bfa3889c 100644 --- a/docs/references/index.rst +++ b/docs/references/index.rst @@ -3,5 +3,3 @@ References .. toctree:: :maxdepth: 1 - - authorization-schema From 932025f498ab50ec57024bb91418802a709bd4ab Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 1 Sep 2026 18:20:57 +0200 Subject: [PATCH 3/5] docs: clarify Paragon icon validation --- docs/decisions/0017-static-authorization-schema.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/0017-static-authorization-schema.rst b/docs/decisions/0017-static-authorization-schema.rst index ca94ad24..063f70a7 100644 --- a/docs/decisions/0017-static-authorization-schema.rst +++ b/docs/decisions/0017-static-authorization-schema.rst @@ -102,7 +102,7 @@ Validation first checks each file against the published schema. It rejects: * invalid YAML, unknown fields, missing required fields, and values with the wrong type; * unsupported schema versions; * IDs that contain uppercase letters or unsupported punctuation; -* icon names that are unavailable from ``@openedx/paragon/icons``; and +* icon names that don't follow the available icons from ``@openedx/paragon/icons``; and * display fields that exceed the agreed size limits. After loading every file, validation checks the combined definitions. It rejects: From 917b782452b9282d11dcc41a2191bd39ed4b9725 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 1 Sep 2026 18:25:50 +0200 Subject: [PATCH 4/5] docs: clarify schema migration and extension validation --- docs/decisions/0017-static-authorization-schema.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/decisions/0017-static-authorization-schema.rst b/docs/decisions/0017-static-authorization-schema.rst index 063f70a7..1e8ed84c 100644 --- a/docs/decisions/0017-static-authorization-schema.rst +++ b/docs/decisions/0017-static-authorization-schema.rst @@ -21,6 +21,8 @@ Decision 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``. Open edX publishes a YAML Schema for this format so that editors, CI, and the compiler all apply the same field and validation rules. +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. + #. Permissions and categories ============================= @@ -108,6 +110,7 @@ Validation first checks each file against the published schema. It rejects: After loading every file, validation checks the combined definitions. It rejects: * references to permissions or categories that do not exist; +* a role extension whose role does not exist in the combined definitions; * a role used in a scope where one of its permissions cannot apply; * conflicting definitions or role extensions with the same priority; and * unsupported combinations of schema versions. From 342a6c381b5d3354b68110274ebd397a78117fb2 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Tue, 1 Sep 2026 18:35:37 +0200 Subject: [PATCH 5/5] docs: render decision section numbers --- docs/decisions/0017-static-authorization-schema.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/decisions/0017-static-authorization-schema.rst b/docs/decisions/0017-static-authorization-schema.rst index 1e8ed84c..207478f0 100644 --- a/docs/decisions/0017-static-authorization-schema.rst +++ b/docs/decisions/0017-static-authorization-schema.rst @@ -16,14 +16,14 @@ Casbin remains the authorization engine, and its adapter continues to read and w Decision ******** -#. Schema format and boundary +1. Schema format and boundary ============================= 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``. Open edX publishes a YAML Schema for this format so that editors, CI, and the compiler all apply the same field and validation rules. 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. -#. Permissions and categories +2. Permissions and categories ============================= A permission contains: @@ -61,7 +61,7 @@ For example: Here, ``course_content`` groups the two permissions for display. The complete permission IDs are ``courses.view_course`` and ``courses.delete_course``, while ``course-v1`` is the scope namespace where they apply. Application code uses the complete permission ID, so changing ``display_name`` does not change permission checks. -#. Roles and role extensions +3. Roles and role extensions ============================ A role contains a stable identifier, display name, description, supported scope namespaces, and a list of complete permission identifiers. When an application needs to change an existing role, it uses ``role_extensions``. An extension may add or remove permissions and may replace the role's display name or description. @@ -96,7 +96,7 @@ The compiler also creates a row that links ``course_admin`` to ``courses.delete_ The schema contains static definitions, while user assignments and user-defined roles stay in the application database. Casbin's ``model.conf`` and matcher also remain owned by ``openedx-authz``. -#. Validation and conflicts +4. Validation and conflicts =========================== Validation first checks each file against the published schema. It rejects: