diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c42e3ade..a4537ce4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,18 @@ Change Log Unreleased __________ +[11.2.0] - 2026-04-20 +--------------------- + +Added +~~~~~ + +* New module openedx_events/authz with ROLE_ASSIGNMENT_CREATED and + ROLE_ASSIGNMENT_DELETED events. +* ADR-0018 documents the decision to use dedicated top-level modules for + supporting subdomains, with authz module recognized as a prior instance of the + same concept in https://github.com/openedx/openedx-events/pull/230 + [11.1.1] - 2026-04-06 --------------------- diff --git a/docs/decisions/0018-supporting-subdomain-modules.rst b/docs/decisions/0018-supporting-subdomain-modules.rst new file mode 100644 index 00000000..4ef5fb8f --- /dev/null +++ b/docs/decisions/0018-supporting-subdomain-modules.rst @@ -0,0 +1,85 @@ +.. _ADR-18: + +0018: Supporting Subdomain Modules for Cross-Domain Events +########################################################## + +Status +****** + +**Proposed** + +Context +******* + +Events in ``openedx-events`` are organized into domain modules (e.g., ``learning``, +``course_authoring``) following the Open edX architecture subdomains in +:ref:`Architecture Subdomains Reference`. This works well when an event belongs +to a single subdomain. + +The `edX DDD Bounded Contexts`_ documentation classifies subdomains as core, +supporting, or generic. Supporting subdomains provide capabilities that multiple +core subdomains depend on, without belonging to any of them. ``analytics`` is the +existing example in ``openedx-events``. + +Authorization has the same character: role assignment events originate from +``openedx-authz`` and could be consumed across learning, content authoring, enterprise, +and other areas. Its domain definition is independent of any single application, +so the ``authz`` module introduced in this ADR is classified as supporting. + +Prior to this decision, there was no explicit guidance for supporting subdomain +events, leaving contributors to make ad-hoc placement choices. + +Decision +******** + +We introduce dedicated top-level modules in ``openedx-events`` for supporting +subdomains when their events cannot be meaningfully attributed to a single +existing domain module. + +A supporting subdomain module is warranted when: + +* The subdomain provides a capability that multiple core subdomains depend on. +* The events are meaningful to consumers across existing domain modules without + a clear primary owner among them. +* Placing the events in any single existing domain module would reflect a + current implementation detail rather than a stable domain boundary. + +The ``authz`` module introduced in this branch applies this pattern to +authorization events. The existing ``analytics`` module is recognized as a prior +instance of the same concept. + +Consequences +************ + +1. Supporting subdomain events have a principled home grounded in the Open edX + DDD taxonomy, rather than an arbitrary placement. +2. The pattern is consistent with how ``analytics`` is already organized, and + both are now explicitly grounded in the same classification. +3. Supporting subdomain modules can evolve independently of any single + application's release cycle. +4. Deciding whether a concern qualifies as a supporting subdomain requires + judgment. Without discipline, this can lead to module proliferation. + +Rejected Alternatives +********************* + +* **Place authorization events under ``course_authoring``**: role assignment is + currently performed through an admin console accessed via Studio, but that is + a transitional implementation detail. The admin console is planned to evolve + into its own application, and role assignment is semantically an authorization + concern, not a content authoring one. +* **Create a generic ``admin`` module**: authorization has a self-contained domain + definition that stands independently of any UI surface. "Admin" describes a user + role and an interface where tasks from multiple domains are aggregated - the + tasks themselves belong to their respective domains. +* **Extend an existing module with a subdirectory**: this would misrepresent the + domain ownership of the events and contradict the existing top-level module + structure. + +References +********** + +- `edX DDD Bounded Contexts`_ +- :ref:`Architecture Subdomains Reference` + +.. _edX DDD Bounded Contexts: https://openedx.atlassian.net/wiki/spaces/AC/pages/663224968/edX+DDD+Bounded+Contexts diff --git a/docs/decisions/index.rst b/docs/decisions/index.rst index 67ed465b..5dc33c57 100644 --- a/docs/decisions/index.rst +++ b/docs/decisions/index.rst @@ -24,3 +24,4 @@ Architectural Decision Records (ADRs) 0015-outbox-pattern-and-production-modes 0016-event-design-practices 0017-event-signal-for-external-grader-score-submission + 0018-supporting-subdomain-modules diff --git a/openedx_events/__init__.py b/openedx_events/__init__.py index 00ba1b0c..bbb4c8a3 100644 --- a/openedx_events/__init__.py +++ b/openedx_events/__init__.py @@ -5,4 +5,4 @@ more information about the project. """ -__version__ = "11.1.1" +__version__ = "11.2.0" diff --git a/openedx_events/authz/__init__.py b/openedx_events/authz/__init__.py new file mode 100644 index 00000000..2dc9568e --- /dev/null +++ b/openedx_events/authz/__init__.py @@ -0,0 +1,6 @@ +""" +Package for events related to the Open edX authorization framework. + +This package is not attached to a specific subdomain, as the events defined +here are used across multiple subdomains (supporting). +""" diff --git a/openedx_events/authz/data.py b/openedx_events/authz/data.py new file mode 100644 index 00000000..0a78b0a8 --- /dev/null +++ b/openedx_events/authz/data.py @@ -0,0 +1,26 @@ +"""Data attributes for events related to the authorization framework.""" + +import attr + + +@attr.s(frozen=True) +class RoleAssignmentData: + """ + Data related to a specific role assignment. + + A role assignment represents the assignment of a role to a subject (e.g., user) + within a specific scope (e.g., course, organization). + + Attributes: + operation (str): The operation being performed (e.g., 'created', 'deleted'). + subject (str): The subject to which the role is assigned (e.g., 'user^john_doe'). + role (str): The role that is assigned (e.g., 'course_admin'). + scope (str): The scope in which the role is assigned (e.g., 'course-v1:edX+DemoX+Demo_Course'). + actor_id (int): The database ID of the actor performing the operation, if available. + """ + + operation = attr.ib(type=str) + subject = attr.ib(type=str) + role = attr.ib(type=str) + scope = attr.ib(type=str) + actor_id = attr.ib(type=int, default=None) diff --git a/openedx_events/authz/signals.py b/openedx_events/authz/signals.py new file mode 100644 index 00000000..6856951a --- /dev/null +++ b/openedx_events/authz/signals.py @@ -0,0 +1,36 @@ +""" +Standard Open edX events related to the Open edX authorization framework. + +This module defines signals that are used to notify other parts of the system +about changes or actions related to authorization. +""" + +from openedx_events.authz.data import RoleAssignmentData +from openedx_events.tooling import OpenEdxPublicSignal + +# .. event_type: org.openedx.authz.role_assignment.created +# .. event_name: ROLE_ASSIGNMENT_CREATED +# .. event_key_field: user.pii.username +# .. event_description: Emitted when a role assignment is created in Open edX. +# .. event_data: RoleAssignmentData +# .. event_trigger_repository: openedx/openedx-authz +ROLE_ASSIGNMENT_CREATED = OpenEdxPublicSignal( + event_type="org.openedx.authz.role_assignment.created", + data={ + "role_assignment": RoleAssignmentData, + } +) + + +# .. event_type: org.openedx.authz.role_assignment.deleted +# .. event_name: ROLE_ASSIGNMENT_DELETED +# .. event_key_field: user.pii.username +# .. event_description: Emitted when a role assignment is deleted in Open edX. +# .. event_data: RoleAssignmentData +# .. event_trigger_repository: openedx/openedx-authz +ROLE_ASSIGNMENT_DELETED = OpenEdxPublicSignal( + event_type="org.openedx.authz.role_assignment.deleted", + data={ + "role_assignment": RoleAssignmentData, + } +) diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+authz+role_assignment+created_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+authz+role_assignment+created_schema.avsc new file mode 100644 index 00000000..3e9e864a --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+authz+role_assignment+created_schema.avsc @@ -0,0 +1,41 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "role_assignment", + "type": { + "name": "RoleAssignmentData", + "type": "record", + "fields": [ + { + "name": "operation", + "type": "string" + }, + { + "name": "subject", + "type": "string" + }, + { + "name": "role", + "type": "string" + }, + { + "name": "scope", + "type": "string" + }, + { + "name": "actor_id", + "type": [ + "null", + "long" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.authz.role_assignment.created" +} \ No newline at end of file diff --git a/openedx_events/event_bus/avro/tests/schemas/org+openedx+authz+role_assignment+deleted_schema.avsc b/openedx_events/event_bus/avro/tests/schemas/org+openedx+authz+role_assignment+deleted_schema.avsc new file mode 100644 index 00000000..61123ce2 --- /dev/null +++ b/openedx_events/event_bus/avro/tests/schemas/org+openedx+authz+role_assignment+deleted_schema.avsc @@ -0,0 +1,41 @@ +{ + "name": "CloudEvent", + "type": "record", + "doc": "Avro Event Format for CloudEvents created with openedx_events/schema", + "fields": [ + { + "name": "role_assignment", + "type": { + "name": "RoleAssignmentData", + "type": "record", + "fields": [ + { + "name": "operation", + "type": "string" + }, + { + "name": "subject", + "type": "string" + }, + { + "name": "role", + "type": "string" + }, + { + "name": "scope", + "type": "string" + }, + { + "name": "actor_id", + "type": [ + "null", + "long" + ], + "default": null + } + ] + } + } + ], + "namespace": "org.openedx.authz.role_assignment.deleted" +} \ No newline at end of file