Skip to content

feat: apply OEP-66 (queryset-scoping pattern) across 6 standardized APIs#38847

Open
Abdul-Muqadim-Arbisoft wants to merge 2 commits into
openedx:feat/axim-api_improvementsfrom
edly-io:feat/apply_queryset-scoping_oep_on_apis
Open

feat: apply OEP-66 (queryset-scoping pattern) across 6 standardized APIs#38847
Abdul-Muqadim-Arbisoft wants to merge 2 commits into
openedx:feat/axim-api_improvementsfrom
edly-io:feat/apply_queryset-scoping_oep_on_apis

Conversation

@Abdul-Muqadim-Arbisoft

Copy link
Copy Markdown
Contributor

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:

  • Endpoint accesspermission_classes (may this user call the endpoint?)
  • Record visibility → a ScopingPolicy applied in get_queryset() via
    ScopedQuerysetMixin, resolving the user's accessible scopes in one bulk
    lookup
    (openedx-authz get_scopes_for_*_and_permission) rather than per-row checks
  • User-driven filtering → query params that narrow (never widen) the authorized rows

Each 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

# Area Related PR Change
1 Grading v3 #38726 docs — single-object audit + point-check annotation
2 Xblock v1 #38723 docs — single-object audit (HasCourseAuthorAccess point check)
3 Course Details v3 #38708 docs — single-object audit + point-check annotation
4 Home v3 #38694 feat — shared scoping.py + list-endpoint compliance docs
5 Home v4 #38684 docs — list-endpoint compliance
6 Enrollments v2 #38724 feat — ScopedQuerysetMixin + AdminEnrollmentScopingPolicy on the admin list

What OEP-66 layers were added

  • Grading v3 / Xblock v1 / Course Details v3single-object endpoints
    (partial_update / CRUD-by-key / retrieve+update). No list action, no
    queryset, so the list-scoping layers are out of scope. Each already implements
    OEP-66's single-object point check (inline user_has_course_permission, or
    the HasCourseAuthorAccess permission class), now documented and annotated.
  • Home v3 / Home v4 — genuine list endpoints, but modulestore-backed
    (not a Django ORM queryset). Record visibility is already delegated to openedx-authz
    via get_courses_accessible_to_userget_scopes_for_user_and_permission(COURSES_VIEW_COURSE)
    (one bulk scope-set lookup, merged with legacy roles). Home v3's libraries action
    is scoped by a per-object has_studio_read_access check; its list action carries
    no 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).
  • Enrollments v2EnrollmentsAdminListView is a real ListAPIView over an ORM
    queryset, so it adopts the pattern literally: ScopedQuerysetMixin +
    AdminEnrollmentScopingPolicy applied in get_queryset(), with the form-based user
    filtering moved into filter_queryset(). The policy is a pass-through for platform
    admins (IsAdminUser), documented as the single seam for a narrower authz scope-set
    filter later — no behavior change today. EnrollmentViewSet.list (service-layer
    scoped) and UserRolesView (self-scoped) get compliance notes.

Shared utilities

New reusable module openedx/core/lib/api/scoping.pyScopingPolicy (ABC) and
ScopedQuerysetMixin — so any ORM-backed DRF list view can wire the OEP-66
record-visibility layer without re-implementing it. Adopted by Enrollments v2's admin
list; ready for future ORM-backed endpoints.

Tests added

Test File Cases
ScopingPolicyTests, ScopedQuerysetMixinTests openedx/core/lib/api/tests/test_scoping.py 4
TestEnrollmentsAdminListView openedx/core/djangoapps/enrollments/v2/tests/test_views.py 8

12 new tests total, all mocked + MongoDB-free. The admin-list tests guard 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, and the ADR 0033 Deprecation header. Commits 1-3 and 5
are 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 EnrollmentsAdminListView internally
(get_queryset → base queryset + filter_queryset) but preserves every filter, the
form-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

  • List-scoping mechanism on single-object endpoints (Grading, Xblock, Course
    Details) — no list/queryset to scope; documented as the OEP-66 detail-path point check.
  • Narrowing the admin enrollment listAdminEnrollmentScopingPolicy intentionally
    returns all rows for platform admins; introducing an org-scoped authz filter is a
    future, opt-in change flagged in the policy docstring.
  • ORM rewrite of Home v3/v4 — kept modulestore-backed (in-memory scoping) to avoid a
    data-source change; the ORM mixin is the sibling path for future ORM-backed endpoints.

Test plan

# CMS
pytest cms/djangoapps/contentstore/rest_api/v1/views/tests/test_xblock_viewset.py \
       cms/djangoapps/contentstore/rest_api/v3/views/tests/test_authoring_grading.py \
       cms/djangoapps/contentstore/rest_api/v3/views/tests/test_course_details.py \
       cms/djangoapps/contentstore/rest_api/v3/views/tests/test_home.py \
       cms/djangoapps/contentstore/rest_api/v3/tests/test_home.py \
       cms/djangoapps/contentstore/rest_api/v4/views/tests/test_home.py \
       cms/djangoapps/contentstore/rest_api/v4/tests/test_home.py

# LMS
pytest openedx/core/lib/api/tests/test_scoping.py \
       openedx/core/djangoapps/enrollments/v2/tests/test_views.py \
       openedx/core/djangoapps/enrollments/v2/tests/test_envelope.py
All green (CMS 92, LMS 49); ruff check clean on all changed files.

@Abdul-Muqadim-Arbisoft Abdul-Muqadim-Arbisoft requested a review from a team as a code owner July 4, 2026 20:05
@Abdul-Muqadim-Arbisoft Abdul-Muqadim-Arbisoft self-assigned this Jul 4, 2026
@Abdul-Muqadim-Arbisoft Abdul-Muqadim-Arbisoft force-pushed the feat/apply_queryset-scoping_oep_on_apis branch from e1b225a to ebf36d7 Compare July 4, 2026 21:12
Abdul Muqadim added 2 commits July 6, 2026 23:17
Add openedx/core/lib/api/scoping.py providing the reusable OEP-66
(openedx-proposals#802) record-visibility primitives for DRF list endpoints:

  * ScopingPolicy — an abstract policy that filters a queryset to the rows a
    user may see, delegating scope resolution to the platform authorization
    engine (openedx-authz) in one bulk lookup rather than per-row checks.
  * ScopedQuerysetMixin — applies a view's ``scoping_policy`` on top of
    ``get_queryset()``, raising ``ImproperlyConfigured`` when it is unset.

Includes unit tests (openedx/core/lib/api/tests/test_scoping.py) covering the
mixin's policy application and the ImproperlyConfigured guard.
Wire the OEP-66 (openedx-proposals#802) three-layer separation into the
ORM-backed admin enrollment list (EnrollmentsAdminListView):

  * Endpoint access — permission_classes = (IsAdminUser,).
  * Record visibility — ScopedQuerysetMixin applies AdminEnrollmentScopingPolicy
    to a base ``queryset`` in get_queryset(). The policy is a pass-through for
    platform admins (who see all enrollments), documented as the 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.

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, the ADR 0033 Deprecation header, and the
scoping-policy pass-through.
@Abdul-Muqadim-Arbisoft Abdul-Muqadim-Arbisoft force-pushed the feat/apply_queryset-scoping_oep_on_apis branch from ebf36d7 to 33137f8 Compare July 6, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant