Skip to content
Merged
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------
Expand Down
85 changes: 85 additions & 0 deletions docs/decisions/0018-supporting-subdomain-modules.rst
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/decisions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion openedx_events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
more information about the project.
"""

__version__ = "11.1.1"
__version__ = "11.2.0"
6 changes: 6 additions & 0 deletions openedx_events/authz/__init__.py
Original file line number Diff line number Diff line change
@@ -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).
"""
26 changes: 26 additions & 0 deletions openedx_events/authz/data.py
Original file line number Diff line number Diff line change
@@ -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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bmtcril:

I changed the actor_id here to the database ID instead due to the PII restrictions we're discussing here openedx/openedx-authz#261 (comment). There's an ADR on PII in events: https://github.com/openedx/openedx-events/blob/34d3fce371f14c892d77284f9fcba8a5e67fc544/docs/decisions/0008-signals-with-pii.rst#decision, which recommends shortening data retention to avoid dropping PII for some events.

Following that approach, we could keep using something like the username in events, but avoid storing it in the audit data model. That way we don't need to handle user retirement there, and external systems would rely on their own data retention policies.

If instead we go with the ID, I don’t think that's a big issue, but we’d lose some traceability for integrations like Aspects unless we can cross-reference it with the users table. That's the main trade-off I see.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That change makes sense to me. It's harder to control data retention on events where you don't know what the receivers will do with it.

It's actually better for Aspects to have the user_id, since there is a flag which controls whether PII should be hitting the Aspects database at all. When it's true the system can look up usernames, otherwise it can just show the IDs, but we would have to create new data transforms to get it in there at all right now.

36 changes: 36 additions & 0 deletions openedx_events/authz/signals.py
Original file line number Diff line number Diff line change
@@ -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,
}
)
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "CloudEvent",
"type": "record",
"doc": "Avro Event Format for CloudEvents created with openedx_events/schema",
"fields": [
{
"name": "role_assignment",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question, I’m not familiar with these events. Should we add a suffix like _delete here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The payload is always the same, so we differentiate between created / deleted by the event type instead (namespace for avro). Not sure if I understand the question, though.

"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"
}