Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Status
******

**Draft**
**Accepted**

Context
*******
Expand All @@ -15,7 +15,7 @@ Context

`PR #361`_ attempted to enforce that full truth table directly inside ``PermissionValidationMeView`` and other REST API endpoints, checking the flag per scope on every request. Per `PR #361's own comment thread`_, those endpoints are release-blocking for Verawood, so baking precise per-scope flag logic into them risked correctness and performance on critical paths without enough test coverage across the framework to be confident in time for the release. That approach was reverted, and the team pivoted to `issue #358`_ instead, exposing the flag's raw state through a dedicated endpoint and letting the admin-console MFE apply the simpler #340/#341 rule itself, deferring precise per-scope filtering to a later cycle.

Neither edx-toggles nor edx-platform expose a suitable public API for this client-facing use case. ``/api/toggles/v0/state/`` (`edx_toggles source`_) can expose override data, but it requires Django staff/admin access and exposes flag state broadly (not just ``authz.enable_course_authoring``). ``WaffleFlagOrgOverrideModel.override_value(name, key)`` and its course-level counterpart (`waffle_utils models source`_) each answer for one specific org or course, not "which orgs/courses have an override."
Neither edx-toggles nor openedx-platform expose a suitable public API to consume the flag's state for this use case. ``openedx/core/djangoapps/waffle_utils/views.py``'s ``ToggleStateView``, built on edx-toggles' reporting machinery (`edx_toggles source`_) and wired at ``/api/toggles/v0/state/``, requires ``IsStaff`` and reports the state of every registered toggle at once. ``WaffleFlagOrgOverrideModel.override_value(name, key)`` and its course-level counterpart (`waffle_utils models source`_) each require already knowing which specific org or course to check.

Decision
********
Expand All @@ -31,8 +31,10 @@ Consequences
#. **Release-blocking endpoints stay untouched.** ``PermissionValidationMeView`` and the other endpoints named in PR #361 keep their existing behavior. This endpoint is additive, isolated, low-risk.
#. **One place answers "what's the flag's state right now."** ``get_waffle_flag_states()`` centralizes the lookup, reusing ``enable_authz_course_authoring()`` for the global tier and querying ``WaffleFlagOrgOverrideModel``/``WaffleFlagCourseOverrideModel`` directly for the org/course tiers, since no public API answers "which orgs/courses have an override."
#. **The MFE bears the filtering complexity.** Applying the #340/#341 "any tier on" rule, and any future precise per-course/per-org filtering, is MFE-side logic from here on.
#. **The response isn't scoped to the caller.** Any authenticated user gets every org/course override on the instance; there's no per-user filtering beyond ``IsAuthenticated``. Callers resolve their own course → org → global precedence from the raw lists, the way ``frontend-app-admin-console``'s ``useCourseAuthoringFlag`` hook (`frontend-app-admin-console#176`_) does.
#. **These override queries scan the whole table, unfiltered by any specific org/course.** For instances with many overrides, this is a full-table read on every call. Not a problem at current scale, but worth revisiting if usage grows (see `issue #360`_).
#. **``openedx_authz.utils`` now depends on** ``common.djangoapps.student.roles.enable_authz_course_authoring`` **and** ``openedx.core.djangoapps.waffle_utils.models``, guarded by the same standalone-import pattern already used elsewhere in this repo (``rest_api/utils.py``, ``handlers.py``). This is a temporary, direct edx-platform dependency, tracked as follow-up work under `issue #360`_ (moving the dependency direction so services depend on ``openedx_authz``).
#. **``openedx_authz.utils`` now depends on** ``common.djangoapps.student.roles.enable_authz_course_authoring`` **and** ``openedx.core.djangoapps.waffle_utils.models``, guarded by the same standalone-import pattern already used elsewhere in this repo (``rest_api/utils.py``, ``handlers.py``). This is a temporary, direct openedx-platform dependency, tracked as follow-up work under `issue #360`_ (moving the dependency direction so services depend on ``openedx_authz``).
#. **The exception has a planned end.** `Issue #377`_ tracks removing ``WaffleFlagStatesAPIView`` and ``get_waffle_flag_states()`` when ``authz.enable_course_authoring`` is deprecated. The flag's ``toggle_target_removal_date`` is 2027-06-09, tracked upstream at `openedx-platform#37927`_.

Rejected Alternatives
**********************
Expand All @@ -41,21 +43,40 @@ Rejected Alternatives
Correctness and performance across the whole framework weren't validated in time for a release-blocking change, per PR #361's own comment thread. The simpler #340/#341 rule doesn't need per-scope precision to ship.

**Relying on** ``/api/toggles/v0/state/``
This edx-toggles endpoint can expose override data, but it requires Django staff/admin access and is not suitable for this use case. It also exposes flag state broadly (not just ``authz.enable_course_authoring``), which is a security risk for this use case.
openedx-platform already exposes a generic toggle-state endpoint, ``ToggleStateView`` in ``openedx/core/djangoapps/waffle_utils/views.py``, wired at this URL. Its ``permission_classes`` require ``IsStaff``, which the admin-console MFE's calling user is not guaranteed to be, and it reports the state of every registered toggle at once.

**Building a replacement endpoint outside openedx-authz now, and migrating the admin-console MFE to it.**
Authorization owns endpoints that expose roles, permissions, assignments, or scopes; a course-authoring flag's raw state is none of those, so this endpoint sits outside authorization's domain. But knowing it doesn't belong here doesn't tell us where it does belong, and that question is out of scope for this ADR. Moving the endpoint now would require a new owner, a new endpoint, and a migration in the admin-console MFE. That work is not justified for a flag with a ``toggle_target_removal_date`` of 2027-06-09 (tracked upstream at `openedx-platform#37927`_; see `Issue #377`_). If the flag remains in use past that date, revisit this alternative.

Addendum (2026-07-28)
*********************

`ADR 0016`_ audits every endpoint in ``openedx_authz.rest_api`` by domain vocabulary. Authorization is a supporting subdomain (`ADR 0018 in openedx-events`_), the same tier as Analytics, consumed across learning, content authoring, and enterprise. Domain ownership (does the endpoint expose roles, permissions, assignments, or scopes?) and package placement (is it reusable or tailored to one workflow?) are separate questions. An Admin Console screen aggregating tasks from several domains isn't itself a domain; each task still belongs to whichever domain owns it.

``WaffleFlagStatesAPIView`` fails the domain-ownership question, since a course-authoring flag's state isn't authorization data. ADR 0016 makes that a standing rule. Authorization endpoints must not compute or expose another domain's data, and this is the sole, named exception, kept here rather than moved elsewhere. The exception doesn't extend to any other endpoint.

Whether these reusable endpoints would ever become flag-aware remained open until ADR 0016 settled it as a domain-boundary rule that doesn't depend on release timing. ``PermissionValidationMeView``, ``RoleUserAPIView``, and ``RoleListView`` must not depend on the course-authoring flag. Any future flag-aware behavior for these endpoints belongs to whichever domain ends up owning the flag.

References
**********

* `ADR 0010`_
* `ADR 0016`_
* `ADR 0018 in openedx-events`_
* `Issue #340`_
* `Issue #341`_
* `Issue #358`_
* `Issue #360`_
* `Issue #377`_
* `PR #361`_
* `PR #361's own comment thread`_
* `A review comment on frontend-app-admin-console#176`_
* `frontend-app-admin-console#176`_
* `openedx-platform#37927`_

.. _ADR 0010: 0010-course-authoring-flag.rst
.. _ADR 0016: 0016-rest-api-domain-ownership-boundary.rst
.. _ADR 0018 in openedx-events: https://github.com/openedx/openedx-events/blob/main/docs/decisions/0018-supporting-subdomain-modules.rst
.. _Issue #340: https://github.com/openedx/openedx-authz/issues/340
.. _issue #340: https://github.com/openedx/openedx-authz/issues/340
.. _Issue #341: https://github.com/openedx/openedx-authz/issues/341
Expand All @@ -64,8 +85,11 @@ References
.. _issue #358: https://github.com/openedx/openedx-authz/issues/358
.. _Issue #360: https://github.com/openedx/openedx-authz/issues/360
.. _issue #360: https://github.com/openedx/openedx-authz/issues/360
.. _Issue #377: https://github.com/openedx/openedx-authz/issues/377
.. _PR #361: https://github.com/openedx/openedx-authz/pull/361
.. _PR #361's own comment thread: https://github.com/openedx/openedx-authz/pull/361#issuecomment-4967053225
.. _A review comment on frontend-app-admin-console#176: https://github.com/openedx/frontend-app-admin-console/pull/176#issuecomment-4900922914
.. _frontend-app-admin-console#176: https://github.com/openedx/frontend-app-admin-console/pull/176
.. _edx_toggles source: https://github.com/openedx/edx-toggles/blob/master/edx_toggles/toggles/state/internal/report.py
.. _waffle_utils models source: https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/waffle_utils/models.py
.. _waffle_utils models source: https://github.com/openedx/openedx-platform/blob/master/openedx/core/djangoapps/waffle_utils/models.py
.. _openedx-platform#37927: https://github.com/openedx/openedx-platform/issues/37927
68 changes: 68 additions & 0 deletions docs/decisions/0016-rest-api-domain-ownership-boundary.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
0016: REST API Domain Ownership Boundary
#########################################

Status
******

**Draft**

Context
*******

``openedx_authz.rest_api`` contains both reusable authorization endpoints and endpoints tailored to a specific Admin Console workflow. That difference in audience and shape does not, by itself, determine which domain owns an endpoint. Issue #360 raised the need for a clearer boundary after `PR #361`_ proposed making ``PermissionValidationMeView`` inspect a course-authoring flag stored in openedx-platform.

An audit of all ten endpoints in ``openedx_authz.rest_api.v1`` classified each one against Open edX's existing domain vocabulary, instead of by who calls it today:

- The `edX DDD Bounded Contexts`_ documentation classifies Open edX's subdomains as core, supporting, or generic.
- `ADR 0018 in openedx-events`_ classifies authorization as a supporting subdomain, the same tier as Analytics. Both are independent of any single application, and both are consumed across learning, content authoring, enterprise, and other areas. ADR 0018 also establishes that a UI surface aggregating tasks from several domains, such as an admin console, is not itself a domain: "'Admin' describes a user role and an interface where tasks from multiple domains are aggregated, the tasks themselves belong to their respective domains."

The audit therefore separates two independent questions:

- **Domain ownership:** Does the endpoint expose authorization data, such as roles, permissions, assignments, or scopes?
- **Package placement:** Is the endpoint reusable, or is it tailored to an Admin Console workflow?

Seven of the ten endpoints expose authorization data. Their domain ownership does not change when an Admin Console-specific response shape makes a separate package useful. ``WaffleFlagStatesAPIView`` exposes the state of a course-authoring flag instead, and this ADR does not need to decide which domain should own that flag; it only establishes that authorization does not. The ownership of ``UserValidationAPIView`` and ``AdminConsoleOrgsAPIView`` also remains unresolved because it is not necessary to settle the flag boundary.

Decision
********

1. Authorization owns endpoints that expose roles, permissions, assignments, or scopes. This remains true whether an endpoint is reusable or tailored to one screen.
2. The five endpoints tailored to Admin Console workflows (``AdminConsoleOrgsAPIView``, ``ScopesAPIView``, ``TeamMembersAPIView``, ``TeamMemberAssignmentsAPIView``, and ``AssignmentsAPIView``) move to ``openedx_authz/rest_api/v1/admin_console/``. This is a code-organization boundary, not a new domain boundary.
3. The reusable endpoints ``PermissionValidationMeView``, ``RoleUserAPIView``, and ``RoleListView`` remain in ``openedx_authz/rest_api/v1/views.py``. Their guarantee is that they answer "what can this subject do" identically regardless of caller. Depending on another domain's concept, the course-authoring flag included, would make that answer depend on who's asking, breaking the guarantee for every other consumer. Any consumer that needs flag-aware behavior handles it on its own side, the way ``authz_permission_required`` already does in CMS.
4. Authorization endpoints must not compute or expose data owned by another domain. ``WaffleFlagStatesAPIView`` is the sole current exception. `ADR 0015`_ documents why it will remain temporarily; the exception must not be extended to other endpoints.
5. This ADR does not decide the domain ownership of ``UserValidationAPIView`` or ``AdminConsoleOrgsAPIView``. They retain the package placement described above until that question is resolved separately.

Consequences
************

1. Future proposals cannot add another domain's logic or data to an authorization endpoint without revisiting this decision.
2. Admin Console-specific endpoints have a clear package boundary without treating the Admin Console as a domain.
3. The ownership of ``UserValidationAPIView`` and ``AdminConsoleOrgsAPIView`` remains open.
4. The package move does not change any URLs, so ``frontend-app-admin-console`` is unaffected.
5. This holds even while openedx-authz has few consumers and one exception looks cheap. `openedx_catalog`_, a generic library in openedx-core, defers the same kind of decision rather than building it early. Its API docstring says it "does not yet provide any 'list courses' methods" because visibility depends on "instance-specific logic (e.g. enterprise, subscriptions, white labelling)," left for a future pluggable extension point instead of the generic API absorbing it now. Taking on one consumer's condition today adds a responsibility the framework doesn't own, and makes the API harder to keep generic as more consumers arrive.

Rejected Alternatives
*********************

**Treating the Admin Console as its own domain, on par with authorization or Course Authoring.**
Per `ADR 0018 in openedx-events`_, a UI surface that aggregates tasks from several domains is not itself a domain; the tasks it aggregates belong to whichever domain already owns them. Classifying the Admin Console as a domain would misattribute ownership of tasks (role assignment, permission checks) that belong to authorization, and introduce vocabulary the rest of the Open edX architecture doesn't use.

**Moving the five Admin Console endpoints out of openedx-authz entirely, into a different repository or application.**
Their data (roles, permissions, assignments, scopes) is authorization's own. A non-reusable shape is a package-organization concern, not a reason to relocate authorization's own data to a different codebase.

References
**********

* `edX DDD Bounded Contexts`_
* `ADR 0018 in openedx-events`_
* `ADR 0015`_
* `Issue #360`_
* `PR #361`_
* `openedx_catalog`_

.. _edX DDD Bounded Contexts: https://openedx.atlassian.net/wiki/spaces/AC/pages/663224968/edX+DDD+Bounded+Contexts
.. _ADR 0018 in openedx-events: https://github.com/openedx/openedx-events/blob/main/docs/decisions/0018-supporting-subdomain-modules.rst
.. _ADR 0015: 0015-expose-course-authoring-waffle-flag-state-via-rest-api.rst
.. _Issue #360: https://github.com/openedx/openedx-authz/issues/360
.. _PR #361: https://github.com/openedx/openedx-authz/pull/361
.. _openedx_catalog: https://github.com/openedx/openedx-core/blob/main/src/openedx_catalog/api.py
48 changes: 48 additions & 0 deletions docs/decisions/0017-rest-api-package-layout-convention.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
0017: Package Layout Convention for REST API Views
###################################################

Status
******

**Draft**

Context
*******

``openedx_authz/rest_api/v1/`` started as a single ``views.py`` module holding every REST endpoint, reusable and consumer-specific alike. `ADR 0016`_ determined which endpoints expose authorization's own data (roles, permissions, assignments, scopes) and which don't, and applied that determination by moving endpoints into two new subpackages: ``admin_console/`` for the five endpoints shaped around the Admin Console's specific workflows, and ``course_authoring/`` for ``WaffleFlagStatesAPIView``, the one documented exception whose data belongs to Course Authoring.

Both moves happened as part of applying ADR 0016's ownership rule, but the packaging convention itself was never written down on its own. This ADR names that convention so a future endpoint has a clear placement rule to apply, instead of requiring a fresh architecture discussion each time one is added.

Decision
********

1. A reusable authorization endpoint, one that answers the same question identically for any caller, stays in ``rest_api/v1/views.py``.
2. An endpoint shaped around one specific consumer's workflow, whose data is still authorization's own, moves into a subpackage named after that consumer, for example ``admin_console/``. This is a package-placement decision. `ADR 0016`_ already governs which data counts as authorization's own; this rule only decides where an endpoint's code lives once that question is settled.
3. An endpoint that is a documented exception to domain ownership (`ADR 0016`_ decision item 4) moves into a subpackage named after the domain its data actually belongs to, for example ``course_authoring/``, rather than staying in ``views.py`` next to endpoints it doesn't behave like.
4. A subpackage owns its ``views.py``, plus its own ``filters.py`` or ``serializers.py`` for classes not shared with the generic module. Mixins and serializers used by both the generic module and a subpackage stay in ``rest_api/v1/serializers.py``.
5. Each subpackage's ``__init__.py`` states in one short docstring which of rule 2 or rule 3 justifies its existence, so a reader can tell why the package exists without reconstructing the reasoning from the ADRs.

Consequences
************

1. A new endpoint has an immediate placement rule. Reusable, authorization's own data stays in ``views.py``. Consumer-specific, authorization's own data gets a subpackage named after that consumer. Data belonging to a different domain gets a subpackage named after that domain.
2. ``admin_console/`` and ``course_authoring/`` are the first two instances of this pattern and can be pointed to directly as examples, instead of re-deriving the reasoning from `ADR 0016`_ for every new case.
3. This ADR does not add a new domain-ownership rule. Package boundaries follow from `ADR 0016`_'s ownership determination.

Rejected Alternatives
*********************

**Deciding placement ad hoc for each new endpoint, without naming the rule.**
This already happened once. The ``admin_console/`` and ``course_authoring/`` moves were correct applications of `ADR 0016`_, but the general rule behind them was never written down as its own decision. Leaving it unstated risks each future endpoint getting a different, undocumented rationale for where it lives.

**One subpackage per domain, regardless of consumer.**
Grouping every Admin-Console-shaped endpoint and every other consumer-specific endpoint into a single package by domain alone would still mix unrelated consumers together, the same problem the original ``views.py`` had. Naming a subpackage after the actual consumer keeps its audience unambiguous.

References
**********

* `ADR 0016`_
* `ADR 0015`_

.. _ADR 0016: 0016-rest-api-domain-ownership-boundary.rst
.. _ADR 0015: 0015-expose-course-authoring-waffle-flag-state-via-rest-api.rst
1 change: 0 additions & 1 deletion openedx_authz/models/authz_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ def save(self, *args, **kwargs) -> "AuthzCourseAuthoringMigrationRun":
super().save(*args, **kwargs)
return self

# pylint: disable=too-many-positional-arguments
@classmethod
def _create(
cls, migration_type, scope_type, scope_key, status, metadata=None
Expand Down
Loading