Skip to content

fix: serve all_blocks unfiltered to users who can access all blocks - #515

Merged
macdiesel merged 4 commits into
release-ulmofrom
bbeggs/ENT-12225-all-blocks-unfiltered-for-staff
Oct 22, 2026
Merged

macdiesel merged 4 commits into
release-ulmofrom
bbeggs/ENT-12225-all-blocks-unfiltered-for-staff

Conversation

@macdiesel

Copy link
Copy Markdown
Member

Description

BlockListGetForm._clean_requested_user serves an all_blocks request as the requesting user whenever that user has instructor.research. That branch was added in openedx#27935 so data researchers could use all_blocks. Global staff also hold instructor.research (instructor/permissions.py: is_staff | HasRolesRule('data_researcher')), so since then global-staff callers get their own per-user view instead of the unfiltered tree. The most visible effect: LibraryContentTransformer doesn't exempt staff, so an item bank (library_content/itembank) returns one random selection of max_count children for that user instead of the full pool.

This change serves the request as the requesting user only when they have instructor.research but can't access all blocks (permissions.can_access_all_blocks, i.e. course staff):

Who this affects: global-staff callers of all_blocks, such as reporting and analytics integrations that export course structure, and the instructor dashboard's problem browser when the instructor is global staff. Course staff who aren't global staff already get this behavior.

Behavior differences for those callers:

  • every child of every item bank is listed, instead of a random per-user selection
  • staff-only blocks are no longer listed: with user=None only VisibilityTransformer runs, and it removes them, which is what course staff already see
  • per-user restrictions (enrollment track groups, start dates) aren't applied, which matches the intent of all_blocks

Testing instructions

  • New form tests in lms/djangoapps/course_api/blocks/tests/test_forms.py: global staff + all_blocks → user is None; a data researcher who isn't course staff → served as themselves.
  • The existing view tests (test_no_user_staff_all_blocks, test_extra_field_*, test_data_researcher_access) should still pass; the toy course has no staff-only blocks, so the staff responses are unchanged.
  • Manual: as a global staff user, for a course with an item bank, request /api/courses/v1/blocks/?course_id=<course>&all_blocks=true&depth=all&requested_fields=children. The item bank should list every child, and repeat requests should return the same children.

Since the data researcher change (openedx#27935), a requesting user with
instructor.research gets all_blocks served as themselves. Global staff
also hold instructor.research, so staff callers stopped getting the
unfiltered tree and started getting per-user transformers, including a
random item bank (library_content) selection in place of the full pool.

Serve as the requesting user only when they have instructor.research but
can't access all blocks, so data researchers keep the access openedx#27935
added and course staff get the unfiltered tree again.

The warehouse course structure export calls this endpoint as a staff
service user, so it currently records one random selection of each
library container's children and misses completions on the rest
(ENT-12225).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The permission logic matches the intended behavior and is covered by focused regression tests.

0 open findings

What changed in this PR

Updates all_blocks permission handling so eligible staff receive the unfiltered course tree while data researchers retain their per-user view.

Changes:

  • Distinguishes data researchers from users who can access all blocks.
  • Adds form tests for global staff and data researchers.
File Description
lms/​djangoapps/​course_api/​blocks/​forms.py Corrects user selection for all_blocks.
lms/​djangoapps/​course_api/​blocks/​tests/​test_forms.py Verifies staff and researcher behavior.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lms/djangoapps/course_api/blocks/forms.py Outdated

@pwnage101 pwnage101 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looking for a more robust and semantic implementation that results in only calling can_access_all_blocks() once.

Move the instructor.research check into _verify_all_blocks so that
can_access_all_blocks is evaluated once and the order of the checks is
explicit: users who can access all blocks get the unfiltered tree, data
researchers who can't are served as themselves, and anyone else is
denied. Behavior is unchanged from the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@macdiesel
macdiesel marked this pull request as ready for review October 22, 2026 12:18
@macdiesel
macdiesel requested a review from pwnage101 October 22, 2026 12:39
@macdiesel
macdiesel enabled auto-merge October 22, 2026 13:59

@brobro10000 brobro10000 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consolidated review (AI-assisted reviewer aid, not an approval)

Summary: The reorder is correct and minimal. The set of users allowed through all_blocks is unchanged (instructor.research ∪ staff access). Only the view served to staff-access users changes, which restores the pre-openedx#27935 behavior for global staff. I found no security or logic defects, and CI is green on 4c8e0c4. There are two inline comments: one blocking docstring fix and one deferrable test cleanup.

Needs a decision before merge

  1. Accept the global-staff response change. With user=None, _has_access_to_course(None, …) denies staff access (access.py#L736-L738), so VisibilityTransformer drops staff-only blocks (visibility.py#L64-L73) and per-user filters are skipped. These in-repo callers change for global staff:

    • the instructor-dashboard BlockBrowser (client.js#L24)
    • remote content-library import (courseware_import.py#L287), which will no longer export staff-only blocks when its OAuth user is global staff

    Recommendation: accept it. It matches course staff and the pre-2021 behavior, and the description already discloses it. Worth confirming with the ENT-12225 requester that their consumer doesn't need staff-only blocks.

Non-blocking
2. Upstream. openedx/edx-platform master still has the research-first order (forms.py#L141-L143), so this fix exists only on the fork's release-ulmo. An upstream PR or tracking ticket would keep it from being lost at the next release rebase.
3. Endpoint-level regression test (deferrable). The form test locks in the mechanism (user is None), but nothing asserts the observable outcome. A view test modeled on test_no_user_staff_all_blocks (test_views.py#L274-L278) would close that gap: build an item bank (max_count=1, ≥2 children) and a visible_to_staff_only unit, request as admin_user, and assert that every bank child is present and the staff-only unit is absent. Fixture patterns: test_library_content.py#L29, test_visibility.py#L34-L39.

Process: the open changes-requested review on 343ddfeb asked for the shape that 4c8e0c4 now has, so it needs a re-review to clear. The title still says [WIP].

Comment on lines +179 to +183
Returns None (the unfiltered tree) for users who can access all blocks, and the requesting user for data
researchers who can't, so they see the blocks as themselves.
"""
# Verify all blocks can be accessed for the course.
if not permissions.can_access_all_blocks(requesting_user, course_key):
raise PermissionDenied(
"'{requesting_username}' does not have permission to access all blocks in '{course_key}'.".format(
requesting_username=requesting_user.username,
course_key=str(course_key),
)
)
# Check this before instructor.research: global staff hold both, and serving them as themselves applies
# per-user transformers, e.g. a random item bank selection instead of the full pool.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

BLOCKING (one-line docstring fix)

The docstring and comment describe None as "the unfiltered tree", but VisibilityTransformer still runs for user=None and removes staff-only blocks (api.py#L84-L94, visibility.py#L64-L73, access.py#L736-L738). This function is where the access decision is made, so a reader would assume global staff now see everything, which is the opposite of the trade-off the PR description lays out.

Suggested wording: Returns None (the tree without per-user transformers; only VisibilityTransformer runs, so staff-only blocks are excluded, matching course staff) for users who can access all blocks, …. Optionally fix the same inaccuracy in the get_blocks docstring (api.py#L39-L40).

self.form_data.pop('username')
self.assert_error('username', "This field is required unless all_blocks is requested.")

def test_all_blocks_by_staff_is_unfiltered(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

DEFERRABLE (follow-up/tech debt)

This repeats the setup of test_no_user_all_blocks_true (L110-L116). Adding assert form.cleaned_data['user'] is None there covers global staff. The other group this reorder changes has no test: course staff (not global) who also hold the data-researcher role. They were served as themselves before and now get None. Suggest repurposing this test: add one user to both CourseStaffRole(self.course.id) and CourseDataResearcherRole(self.course.id) (same setup as L141-L147) and assert user is None.

Safe to defer: the change is test-only, the precedence branch is already exercised by the global-staff case, and CI is green.

macdiesel and others added 2 commits October 22, 2026 11:28
With user=None, get_blocks runs only VisibilityTransformer, and staff
access is denied for a missing user, so blocks visible to staff only are
removed. The _verify_all_blocks and get_blocks docstrings described this
as the unfiltered tree.

Also assert user is None for global staff in the existing all_blocks
test, and replace the duplicate global-staff test with one for course
staff who also hold the data researcher role, the other group whose
response this change affects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add view tests over a course with an item bank (max_count=1, two
children) and a staff-only unit. Global staff requesting all_blocks get
every item bank child, where before the fix they got one random
selection. A data researcher still gets their own single selection.
Neither sees the staff-only unit, since user=None still runs
VisibilityTransformer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brobro10000
brobro10000 disabled auto-merge October 22, 2026 16:39

@brobro10000 brobro10000 left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approving. I accept that global-staff all_blocks callers now lose staff-only blocks (same as course staff). Please confirm with the ENT-12225 consumer and open an upstream openedx ticket. Also, the title still says [WIP]: please drop it once this is final (or say if it is not), so it is clear the PR is ready to merge.

@macdiesel macdiesel changed the title [WIP] fix: serve all_blocks unfiltered to users who can access all blocks fix: serve all_blocks unfiltered to users who can access all blocks Oct 22, 2026
@macdiesel
macdiesel enabled auto-merge October 22, 2026 16:44
@macdiesel
macdiesel dismissed pwnage101’s stale review October 22, 2026 16:44

Hamzah reviewed and we made Troy's requested change.

@macdiesel
macdiesel merged commit 85b545b into release-ulmo Oct 22, 2026
102 checks passed
@macdiesel
macdiesel deleted the bbeggs/ENT-12225-all-blocks-unfiltered-for-staff branch October 22, 2026 17:10
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.

4 participants