diff --git a/MANIFEST.in b/MANIFEST.in index d998b09e..a066ee05 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,4 @@ include LICENSE.txt include README.rst include requirements/base.in include requirements/constraints.txt -recursive-include openedx_authz *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.conf *.policy *.yaml *.yml +recursive-include openedx_authz *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.conf *.policy *.yaml *.yml *.json diff --git a/docs/conf.py b/docs/conf.py index 75e02d1a..bb785b8e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,8 +71,13 @@ def get_version(*file_paths): "sphinx.ext.intersphinx", "sphinx.ext.ifconfig", "sphinx.ext.napoleon", + "sphinx-jsonschema", ] +jsonschema_options = { + "auto_reference": True, +} + # A list of warning types to suppress arbitrary warning messages. suppress_warnings = [ "image.nonlocal_uri", @@ -570,6 +575,7 @@ def on_init(app): # pylint: disable=unused-argument docs_path, os.path.join(root_path, "openedx_authz"), os.path.join(root_path, "openedx_authz/migrations"), + os.path.join(root_path, "openedx_authz/schema"), os.path.join(root_path, "openedx_authz/tests"), ], env=env diff --git a/docs/decisions/0017-static-authorization-schema.rst b/docs/decisions/0017-static-authorization-schema.rst index c2e77fef..8f95b10a 100644 --- a/docs/decisions/0017-static-authorization-schema.rst +++ b/docs/decisions/0017-static-authorization-schema.rst @@ -19,10 +19,12 @@ Decision 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``, which apply to all definitions in that file. Priority is used when role extensions conflict. 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 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 when role extensions conflict. Open edX publishes a :download:`machine-readable JSON Schema <../../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. 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 ============================= @@ -159,11 +161,11 @@ References * `ADR 0016`_ * `ADR 0023`_ * `Casbin adapters`_ -* `ASDF YAML Schema`_ +* `JSON Schema`_ * `Paragon icons`_ .. _ADR 0016: 0016-static-and-dynamic-roles.rst .. _ADR 0023: https://github.com/openedx/openedx-authz/pull/430 .. _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 +.. _JSON Schema: https://json-schema.org/draft/2020-12/json-schema-core .. _Paragon icons: https://paragon-openedx.netlify.app/components/icon/ diff --git a/docs/decisions/0018-authorization-schema-lifecycle.rst b/docs/decisions/0018-authorization-schema-lifecycle.rst index e83e3ac8..82f82148 100644 --- a/docs/decisions/0018-authorization-schema-lifecycle.rst +++ b/docs/decisions/0018-authorization-schema-lifecycle.rst @@ -62,7 +62,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. 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-json.rst b/docs/references/authorization-schema-json.rst new file mode 100644 index 00000000..a67f9827 --- /dev/null +++ b/docs/references/authorization-schema-json.rst @@ -0,0 +1,118 @@ +.. _Generated Authorization Schema: + +Generated Authorization Schema +############################## + +This page renders the exact validation rules from the :download:`authz JSON Schema <../../openedx_authz/schema/authz-schema-v1.json>`. See the :doc:`Authorization Schema Reference ` for guidance, examples, and validation performed by the compiler across schema contributions. + +Top-level fields +**************** + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/properties/schema_version + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/properties/priority + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/properties/permission_categories + :lift_description: + :hide_key: /items + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/properties/permissions + :lift_description: + :hide_key: /items + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/properties/roles + :lift_description: + :hide_key: /items + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/properties/role_extensions + :lift_description: + :hide_key: /items + +Permission category fields +************************** + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission_category/properties/id + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission_category/properties/display_name + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission_category/properties/description + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission_category/properties/icon + :lift_description: + +Permission fields +***************** + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission/properties/namespace + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission/properties/name + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission/properties/display_name + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission/properties/description + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission/properties/category + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission/properties/scopes + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/permission/properties/icon + :lift_description: + +Role fields +*********** + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role/properties/id + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role/properties/display_name + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role/properties/description + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role/properties/scopes + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role/properties/permissions + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role/properties/icon + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role/properties/hidden + :lift_description: + +Role extension fields +********************* + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role_extension/properties/role + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role_extension/properties/add_permissions + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role_extension/properties/remove_permissions + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role_extension/properties/display_name + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role_extension/properties/description + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role_extension/properties/icon + :lift_description: + +.. jsonschema:: ../../openedx_authz/schema/authz-schema-v1.json#/$defs/role_extension/properties/hidden + :lift_description: diff --git a/docs/references/authorization-schema.rst b/docs/references/authorization-schema.rst new file mode 100644 index 00000000..c92a8a4d --- /dev/null +++ b/docs/references/authorization-schema.rst @@ -0,0 +1,362 @@ +.. _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: + +.. toctree:: + :hidden: + + authorization-schema-json + +Machine-readable schema +*********************** + +The :download:`downloadable authz JSON Schema <../../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 ` or `view the schema source `_. Editors and validation tools can use it to check YAML files because JSON Schema applies to the data represented by both JSON and YAML. + +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. + +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 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. + +``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 can keep schema resources under an ``authz/schema`` package directory. Filenames are chosen by the application, but separating permission and role definitions makes the resources easier to maintain: + +.. code-block:: text + + course_authoring/ + └── authz/ + └── schema/ + ├── permissions.yaml + └── roles.yaml + +The application exposes the ``authz/schema`` directory through the ``openedx-authz`` schema entry point described in :doc:`ADR 0019 <../decisions/0019-authorization-schema-discovery>`. The directory path is relative to the Python module, which keeps discovery independent of virtual-environment and container paths. The compiler loads every ``.yaml`` file in that directory. + +Tutor configuration for site operators +************************************** + +A site operator can provide an authz schema through a Python Tutor plugin that uses the ``openedx-authz-schema`` patch. Run ``tutor plugins printroot`` to find the local plugin directory, then create ``openedx_authz_overrides.py`` there: + +.. code-block:: python + + from tutor import hooks + + hooks.Filters.ENV_PATCHES.add_item(( + "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 diff --git a/openedx_authz/schema/__init__.py b/openedx_authz/schema/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openedx_authz/schema/authz-schema-v1.json b/openedx_authz/schema/authz-schema-v1.json new file mode 100644 index 00000000..4e7956ef --- /dev/null +++ b/openedx_authz/schema/authz-schema-v1.json @@ -0,0 +1,399 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/openedx/openedx-authz/main/openedx_authz/schema/authz-schema-v1.json", + "title": "Open edX Authorization schema", + "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.", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "priority"], + "properties": { + "schema_version": { + "title": "schema_version", + "description": "The authorization schema format version. Quote this value in YAML so it remains a string.", + "type": "string", + "const": "1.0", + "examples": ["1.0"] + }, + "priority": { + "title": "priority", + "description": "The precedence used when contributions extend the same role. A higher number takes precedence when extensions conflict.", + "type": "integer", + "examples": [100] + }, + "permission_categories": { + "title": "permission_categories", + "description": "Permission categories contributed by this file. Categories group permissions for display and discovery but do not grant access.", + "type": "array", + "items": { + "$ref": "#/$defs/permission_category" + }, + "minItems": 1, + "examples": [ + [ + { + "id": "course_content", + "display_name": "Course content", + "description": "Permissions for viewing and editing course content.", + "icon": "Article" + } + ] + ] + }, + "permissions": { + "title": "permissions", + "description": "Static permissions contributed by this file.", + "type": "array", + "items": { + "$ref": "#/$defs/permission" + }, + "minItems": 1, + "examples": [ + [ + { + "namespace": "courses", + "name": "view_course", + "display_name": "View course", + "description": "View course configuration and content.", + "category": "course_content", + "scopes": ["course-v1"], + "icon": "Visibility" + } + ] + ] + }, + "roles": { + "title": "roles", + "description": "Static roles contributed by this file.", + "type": "array", + "items": { + "$ref": "#/$defs/role" + }, + "minItems": 1, + "examples": [ + [ + { + "id": "course_observer", + "display_name": "Course observer", + "description": "Reviews a course without changing it.", + "scopes": ["course-v1"], + "permissions": ["courses.view_course"] + } + ] + ] + }, + "role_extensions": { + "title": "role_extensions", + "description": "Changes to static roles defined in this file or another contribution.", + "type": "array", + "items": { + "$ref": "#/$defs/role_extension" + }, + "minItems": 1, + "examples": [ + [ + { + "role": "course_editor", + "add_permissions": ["courses.export_course"] + } + ] + ] + } + }, + "anyOf": [ + {"required": ["permission_categories"]}, + {"required": ["permissions"]}, + {"required": ["roles"]}, + {"required": ["role_extensions"]} + ], + "$defs": { + "identifier": { + "description": "A stable lowercase snake-case identifier that begins with a letter.", + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "examples": ["course_content"] + }, + "permission_id": { + "description": "A complete permission identifier formed from its namespace and name, separated by a period.", + "type": "string", + "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$", + "examples": ["courses.view_course"] + }, + "scope_namespace": { + "description": "A scope namespace registered by a ScopeData type.", + "type": "string", + "minLength": 1, + "examples": ["course-v1"] + }, + "display_name": { + "description": "The source-language name shown to users.", + "type": "string", + "minLength": 1, + "examples": ["View course"] + }, + "description": { + "description": "A source-language description shown to users.", + "type": "string", + "minLength": 1, + "examples": ["View course configuration and content."] + }, + "icon": { + "description": "A case-sensitive icon name exported by @openedx/paragon/icons.", + "type": "string", + "minLength": 1, + "examples": ["Visibility"] + }, + "permission_list": { + "description": "A non-empty list of unique, complete permission identifiers.", + "type": "array", + "items": { + "$ref": "#/$defs/permission_id" + }, + "minItems": 1, + "uniqueItems": true + }, + "scope_list": { + "description": "A non-empty list of unique scope namespaces.", + "type": "array", + "items": { + "$ref": "#/$defs/scope_namespace" + }, + "minItems": 1, + "uniqueItems": true + }, + "permission_category": { + "title": "Permission category", + "description": "A category used to group permissions for display and discovery.", + "type": "object", + "additionalProperties": false, + "required": ["id", "display_name", "description"], + "properties": { + "id": { + "title": "Category id", + "description": "The global category identifier.", + "examples": ["course_content"], + "$ref": "#/$defs/identifier" + }, + "display_name": { + "title": "Category display_name", + "examples": ["Course content"], + "$ref": "#/$defs/display_name" + }, + "description": { + "title": "Category description", + "examples": ["Permissions for viewing and editing course content."], + "$ref": "#/$defs/description" + }, + "icon": { + "title": "Category icon", + "examples": ["Article"], + "$ref": "#/$defs/icon" + } + } + }, + "permission": { + "title": "Permission", + "description": "A static permission. Its complete identifier is namespace.name.", + "type": "object", + "additionalProperties": false, + "required": ["namespace", "name", "display_name", "description", "category", "scopes"], + "properties": { + "namespace": { + "title": "Permission namespace", + "description": "The stable product domain that owns the permission.", + "examples": ["courses"], + "$ref": "#/$defs/identifier" + }, + "name": { + "title": "Permission name", + "description": "The operation within the product domain, normally beginning with a verb.", + "examples": ["view_course"], + "$ref": "#/$defs/identifier" + }, + "display_name": { + "title": "Permission display_name", + "examples": ["View course"], + "$ref": "#/$defs/display_name" + }, + "description": { + "title": "Permission description", + "examples": ["View course configuration and content."], + "$ref": "#/$defs/description" + }, + "category": { + "title": "Permission category", + "description": "The ID of a permission category in the combined schema.", + "examples": ["course_content"], + "$ref": "#/$defs/identifier" + }, + "scopes": { + "title": "Permission scopes", + "description": "The scope namespaces where this permission can apply.", + "examples": [["course-v1"]], + "$ref": "#/$defs/scope_list" + }, + "icon": { + "title": "Permission icon", + "examples": ["Visibility"], + "$ref": "#/$defs/icon" + } + } + }, + "role": { + "title": "Role", + "description": "A static role and the complete set of permissions assigned to it.", + "type": "object", + "additionalProperties": false, + "required": ["id", "display_name", "description", "scopes", "permissions"], + "properties": { + "id": { + "title": "Role id", + "description": "The stable role identifier.", + "examples": ["course_observer"], + "$ref": "#/$defs/identifier" + }, + "display_name": { + "title": "Role display_name", + "examples": ["Course observer"], + "$ref": "#/$defs/display_name" + }, + "description": { + "title": "Role description", + "examples": ["Reviews a course without changing it."], + "$ref": "#/$defs/description" + }, + "scopes": { + "title": "Role scopes", + "description": "The scope namespaces where this role can be assigned.", + "examples": [["course-v1"]], + "$ref": "#/$defs/scope_list" + }, + "permissions": { + "title": "Role permissions", + "description": "Every permission assigned to this role. Permissions are not inferred from other permissions.", + "examples": [["courses.view_course"]], + "$ref": "#/$defs/permission_list" + }, + "icon": { + "title": "Role icon", + "examples": ["Visibility"], + "$ref": "#/$defs/icon" + }, + "hidden": { + "title": "Role hidden", + "description": "Whether normal role discovery and selection interfaces omit this role.", + "type": "boolean", + "default": false, + "examples": [false] + } + } + }, + "role_extension": { + "title": "Role extension", + "description": "A partial change to an existing static role. Include at least one field in addition to role.", + "type": "object", + "additionalProperties": false, + "required": ["role"], + "properties": { + "role": { + "title": "Extension role", + "description": "The ID of the static role to change.", + "examples": ["course_editor"], + "$ref": "#/$defs/identifier" + }, + "add_permissions": { + "title": "Extension add_permissions", + "description": "Complete permission IDs to add to the role.", + "examples": [["courses.export_course"]], + "$ref": "#/$defs/permission_list" + }, + "remove_permissions": { + "title": "Extension remove_permissions", + "description": "Complete permission IDs to remove from the role.", + "examples": [["courses.manage_tags"]], + "$ref": "#/$defs/permission_list" + }, + "display_name": { + "title": "Extension display_name", + "description": "A replacement source-language display name.", + "examples": ["Course author"], + "$ref": "#/$defs/display_name" + }, + "description": { + "title": "Extension description", + "description": "A replacement source-language description.", + "examples": ["Creates and exports course content."], + "$ref": "#/$defs/description" + }, + "icon": { + "title": "Extension icon", + "description": "A replacement Paragon icon name.", + "examples": ["Edit"], + "$ref": "#/$defs/icon" + }, + "hidden": { + "title": "Extension hidden", + "description": "Whether normal role discovery and selection interfaces omit this role.", + "type": "boolean", + "examples": [true] + } + }, + "anyOf": [ + {"required": ["add_permissions"]}, + {"required": ["remove_permissions"]}, + {"required": ["display_name"]}, + {"required": ["description"]}, + {"required": ["icon"]}, + {"required": ["hidden"]} + ] + } + }, + "examples": [ + { + "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": "export_course", + "display_name": "Export course", + "description": "Export course configuration and content.", + "category": "course_content", + "scopes": ["course-v1"], + "icon": "Download" + } + ], + "roles": [ + { + "id": "course_observer", + "display_name": "Course observer", + "description": "Reviews a course without changing it.", + "scopes": ["course-v1"], + "permissions": ["courses.view_course"] + } + ], + "role_extensions": [ + { + "role": "course_editor", + "add_permissions": ["courses.export_course"] + } + ] + } + ] +} diff --git a/requirements/doc.in b/requirements/doc.in index a8b02901..5b7647da 100644 --- a/requirements/doc.in +++ b/requirements/doc.in @@ -8,3 +8,4 @@ sphinx-book-theme # Common theme for all Open edX projects twine # Validates README.rst for usage on PyPI build # Needed to build the wheel for twine check Sphinx # Documentation builder +sphinx-jsonschema # Render the authz JSON Schema diff --git a/requirements/doc.txt b/requirements/doc.txt index a86223f0..9d17596f 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -184,6 +184,8 @@ jinja2==3.1.6 # code-annotations # pydata-sphinx-theme # sphinx +jsonpointer==3.1.1 + # via sphinx-jsonschema keyring==25.7.0 # via twine markdown-it-py==4.2.0 @@ -327,6 +329,8 @@ sphinx==9.1.0 # sphinx-book-theme sphinx-book-theme==1.4.0 # via -r requirements/doc.in +sphinx-jsonschema==1.19.2 + # via -r requirements/doc.in sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0