feat: apply OEP-66 (queryset-scoping pattern) across 6 standardized APIs#38847
Open
Abdul-Muqadim-Arbisoft wants to merge 6 commits into
Open
Conversation
added 3 commits
July 4, 2026 23:49
Document how the v3 AuthoringGradingViewSet relates to the OEP-66 queryset-scoping pattern for list endpoints (openedx-proposals#802). This endpoint is single-object: partial_update operates on one course identified by course_key and exposes no list action or queryset, so the list-scoping layers (ScopingPolicy / ScopedQuerysetMixin / get_queryset / django-filter) are out of scope. The applicable OEP-66 guidance is the single-object / detail path — a direct object-level point check rather than building a visible-scope set — which the viewset already implements via the inline user_has_course_permission check. Adds a compliance entry to the module docstring and annotates the point check accordingly. Validated: 15/15 v3 grading tests pass in Tutor dev (Python 3.12).
Document how XblockViewSet relates to the OEP-66 queryset-scoping pattern for list endpoints (openedx-proposals#802). This viewset is single-object: every action (create / retrieve / update / partial_update / destroy) operates on one xblock identified by usage_key_string and exposes no list action or queryset, so the list-scoping layers (ScopingPolicy / ScopedQuerysetMixin / get_queryset / django-filter) are out of scope. The applicable OEP-66 guidance is the single-object / detail path — a direct object-level point check — which the viewset already implements: initial() derives the xblock's course_key and the HasCourseAuthorAccess permission class point-checks authoring access to that one course. Adds a compliance entry to the module docstring and annotates permission_classes. Validated: 17/17 xblock v1 viewset tests pass in Tutor dev (Python 3.12).
Document how CourseDetailsViewSet relates to the OEP-66 queryset-scoping pattern for list endpoints (openedx-proposals#802). This viewset is single-object: retrieve and update operate on one course identified by course_id and expose no list action or queryset, so the list-scoping layers (ScopingPolicy / ScopedQuerysetMixin / get_queryset / django-filter) are out of scope. The applicable OEP-66 guidance is the single-object / detail path — a direct object-level point check — which the viewset already implements via the per-action inline user_has_course_permission checks. Adds a compliance entry to the module docstring and annotates the retrieve point check. Validated: 18/18 course_details v3 tests pass in Tutor dev (Python 3.12).
added 3 commits
July 5, 2026 02:07
Add the reusable OEP-66 building blocks and document how the v3 HomeViewSet
list endpoints conform to the queryset-scoping pattern (openedx-proposals#802).
New shared module openedx/core/lib/api/scoping.py provides ScopingPolicy
(ABC) and ScopedQuerysetMixin — the ORM-backed record-visibility layer for
DRF list endpoints, delegating scope resolution to openedx-authz in one bulk
lookup rather than per-row checks.
HomeViewSet is modulestore-backed (no Django ORM queryset), so the mixin
cannot attach. Per OEP-66's "Data sources without an ORM" note, record
visibility is applied in memory by the existing helpers, and differs per
action:
* courses — get_course_context -> get_courses_accessible_to_user resolves
visible courses via one get_scopes_for_user_and_permission(COURSES_VIEW_COURSE)
scope-set lookup (merged with legacy roles).
* libraries — get_library_context -> _accessible_libraries_iter filters with
a per-object has_studio_read_access check (per-object variant, not scope-set).
* list — carries no records (get_home_context(no_course=True) returns only
Studio metadata); nothing to scope.
Endpoint access is IsAuthenticated; ?org=/?is_migrated= are user-driven filters.
Adds a compliance entry to the module docstring, annotates the courses and
libraries data sources, and adds openedx/core/lib/api/tests/test_scoping.py
covering the mixin's policy application and ImproperlyConfigured guard.
Validated in Tutor dev (Python 3.12): home v3 tests + new test_scoping pass.
Document how the v4 HomeCoursesViewSet paginated list endpoint conforms to the OEP-66 queryset-scoping pattern (openedx-proposals#802). Like Home v3, this is a genuine list endpoint that is modulestore-backed (no Django ORM queryset): list sources courses from get_course_context_v2 → get_courses_accessible_to_user, which resolves the user's visible courses in one bulk get_scopes_for_user_and_permission(COURSES_VIEW_COURSE) lookup (merged with legacy roles) — OEP-66's scope-set approach, not per-row checks. Endpoint access is IsAuthenticated; ?org=/?search=/?active_only=/ ?archived_only= are the user-driven filters. Because the source is the modulestore (a list paginated in memory), the shared ORM ScopedQuerysetMixin cannot attach; per OEP-66's "Data sources without an ORM" note the scope is applied in memory by the accessible-courses helper. Adds a compliance entry to the class docstring and annotates the data source. Validated: 27/27 home v4 tests pass in Tutor dev (Python 3.12).
Wire the literal OEP-66 three-layer separation into the ORM-backed admin
enrollment list, document the sibling list surfaces, and add regression
tests (openedx-proposals#802).
EnrollmentsAdminListView is a real ListAPIView over CourseEnrollment, so it
adopts the pattern directly:
* Endpoint access — permission_classes = (IsAdminUser,).
* Record visibility — ScopedQuerysetMixin applies AdminEnrollmentScopingPolicy
to a base `queryset` class attribute in get_queryset(). The policy is a
pass-through for platform admins (who see all enrollments), documented as
the single seam for a narrower openedx-authz scope-set filter later;
behavior is unchanged.
* User-driven filtering — the form-based course_key/course_keys/username/
email/ordering filtering moves from get_queryset() into filter_queryset(),
so it runs after scoping and only narrows the authorized queryset.
EnrollmentViewSet.list resolves record visibility (own vs. staff/api-key) in
the operations service, and UserRolesView is inherently self-scoped, so the
mixin does not attach there; compliance notes are added to both docstrings.
Adds TestEnrollmentsAdminListView regression tests guarding the
get_queryset->filter_queryset split: endpoint access (401/403), pass-through
scoping (admin sees all rows), course_key/username filters still narrow, the
400-on-invalid-params path (validation now in filter_queryset), the ADR 0033
Deprecation header, and the scoping-policy pass-through.
Validated in Tutor dev (Python 3.12): enrollments v2 view + envelope tests
and the new admin-list tests pass.
e1b225a to
ebf36d7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Applies the OEP-66 "Separating Authorization Concerns in List Endpoints"
queryset-scoping pattern (added in openedx-proposals#802)
across the 6 standardized FC-0118 APIs.
OEP-66 keeps three authorization concerns separate on a list endpoint:
permission_classes(may this user call the endpoint?)ScopingPolicyapplied inget_queryset()viaScopedQuerysetMixin, resolving the user's accessible scopes in one bulklookup (openedx-authz
get_scopes_for_*_and_permission) rather than per-row checksEach API is applied and committed on its own (one commit per API). Single-object
endpoints get the ADR's detail-path treatment (a direct point check, documented);
list endpoints get the record-visibility layer, using the ORM mixin where an ORM
queryset exists and the ADR's documented in-memory fallback where the source is the
modulestore.
Commits
HasCourseAuthorAccesspoint check)scoping.py+ list-endpoint compliance docsScopedQuerysetMixin+AdminEnrollmentScopingPolicyon the admin listWhat OEP-66 layers were added
(
partial_update/ CRUD-by-key /retrieve+update). Nolistaction, noqueryset, so the list-scoping layers are out of scope. Each already implements
OEP-66's single-object point check (inline
user_has_course_permission, orthe
HasCourseAuthorAccesspermission class), now documented and annotated.(not a Django ORM queryset). Record visibility is already delegated to openedx-authz
via
get_courses_accessible_to_user→get_scopes_for_user_and_permission(COURSES_VIEW_COURSE)(one bulk scope-set lookup, merged with legacy roles). Home v3's
librariesactionis scoped by a per-object
has_studio_read_accesscheck; itslistaction carriesno records. Per OEP-66's "Data sources without an ORM" note, the scope decision is
applied in memory. Documented per action; the reusable ORM abstractions can't attach
here (no
QuerySet).EnrollmentsAdminListViewis a realListAPIViewover an ORMqueryset, so it adopts the pattern literally:
ScopedQuerysetMixin+AdminEnrollmentScopingPolicyapplied inget_queryset(), with the form-based userfiltering moved into
filter_queryset(). The policy is a pass-through for platformadmins (
IsAdminUser), documented as the single seam for a narrower authz scope-setfilter later — no behavior change today.
EnrollmentViewSet.list(service-layerscoped) and
UserRolesView(self-scoped) get compliance notes.Shared utilities
New reusable module
openedx/core/lib/api/scoping.py—ScopingPolicy(ABC) andScopedQuerysetMixin— so any ORM-backed DRF list view can wire the OEP-66record-visibility layer without re-implementing it. Adopted by Enrollments v2's admin
list; ready for future ORM-backed endpoints.
Tests added
ScopingPolicyTests,ScopedQuerysetMixinTestsopenedx/core/lib/api/tests/test_scoping.pyTestEnrollmentsAdminListViewopenedx/core/djangoapps/enrollments/v2/tests/test_views.py12 new tests total, all mocked + MongoDB-free. The admin-list tests guard the
get_queryset()→filter_queryset()split: endpoint access (401/403), pass-throughscoping (admin sees all rows),
course_key/usernamefilters still narrow, the400-on-invalid-params path, and the ADR 0033
Deprecationheader. Commits 1-3 and 5are documentation-only (no runtime change) and add no tests, consistent with prior
declaration-only ADR commits (e.g. ADR 0034, #38796).
Backward compatibility
No default response or behavior changes. Commits 1-5 are docstring/comment
additions only. Commit 6 restructures
EnrollmentsAdminListViewinternally(
get_queryset→ base queryset +filter_queryset) but preserves every filter, theform-validation 400 path, ordering, and the ADR 0033 header — the scoping policy is a
pass-through, so the same rows are returned. Guarded by the new regression tests above.
Out of scope
Details) — no list/queryset to scope; documented as the OEP-66 detail-path point check.
AdminEnrollmentScopingPolicyintentionallyreturns all rows for platform admins; introducing an org-scoped authz filter is a
future, opt-in change flagged in the policy docstring.
data-source change; the ORM mixin is the sibling path for future ORM-backed endpoints.
Test plan