Skip to content

fix: Course Auditor gets 403 navigating to a course unit - #38986

Open
efortish wants to merge 3 commits into
openedx:masterfrom
eduNEXT:ks/issue-384-auditor-unit-403
Open

fix: Course Auditor gets 403 navigating to a course unit#38986
efortish wants to merge 3 commits into
openedx:masterfrom
eduNEXT:ks/issue-384-auditor-unit-403

Conversation

@efortish

@efortish efortish commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes openedx-authz#384

A user with the Course Auditor role can view the course outline, but gets a 403 Forbidden when opening a specific unit.

Root causes (two, found via local testing against a real devstack)

1. Legacy Studio views (cms/djangoapps/contentstore/views/block.py)

xblock_outline_handler (the outline tree) already uses the AuthZ-aware user_has_course_permission(..., COURSES_VIEW_COURSE.identifier, ..., LegacyAuthoringPermission.READ) check, so it correctly recognizes AuthZ-native roles that have no legacy equivalent — course_auditor and course_editor aren't in LEGACY_COURSE_ROLE_EQUIVALENCES.

xblock_container_handler, xblock_view_handler, and xblock_edit_view were never migrated to that pattern. They called the legacy-only has_studio_read_access directly, which bottoms out in get_user_permissions() (common/djangoapps/student/auth.py) and only grants access based on legacy-mapped roles (staff, instructor, limited_staff, ...).

2. The REST API v1 view the Authoring MFE actually uses (found by testing manually)

The modern Authoring MFE's unit page doesn't call the legacy routes above. It calls GET /api/contentstore/v1/container_handler/{usage_key} (ContainerHandlerView in rest_api/v1/views/vertical_block.py), which still returned 403 after the block.py fix.

That view — along with container_handler, container_embed_handler, and xblock_edit_view's call site — all route through a shared helper, _get_item_in_course() in cms/djangoapps/contentstore/views/component.py, which gated on has_course_author_access (a legacy-only write check), even though every one of its callers only needs read access to render a view. course_auditor never has write access, so it always hit PermissionDenied here regardless of the block.py fix.

Fix

  • Migrate the three remaining read checks in block.py to the same user_has_course_permission pattern xblock_outline_handler already uses.
  • Migrate _get_item_in_course() in component.py from has_course_author_access to the same read-based user_has_course_permission check, fixing all of its callers (including the REST API v1 view) at their single shared choke point. component_handler's own, separate has_course_author_access check — which correctly gates the actual write/persist side effect — is untouched.
  • xblock_handler/handle_xblock (the CRUD endpoint for reading/editing/deleting xblocks) was already correctly AuthZ-aware via _check_xblock_permission, so it needed no change.

Testing instructions

  1. Enable the authz.enable_course_authoring flag globally.
  2. Assign a user the course_auditor role for a course.
  3. Log in as that user, open the course outline, then click into a unit.
  4. Confirm the unit page loads instead of returning a 403.

Verified manually end-to-end, not just with automated tests: mounted this branch into a real devstack, assigned course_auditor to a test user, reproduced the 403 on the unit page before this fix, and confirmed it resolves after it.

Added/updated tests:

  • TestXBlockContainerAndViewHandlerAuthz in cms/djangoapps/contentstore/views/tests/test_block.py, covering xblock_container_handler and xblock_view_handler for course_staff, course_admin, course_auditor, and course_editor, mirroring the existing TestXBlockOutlineHandlerAuthz coverage for the outline handler.
  • ContainerHandlerViewAuthzTest in cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py, covering the REST API v1 ContainerHandlerView for the same set of roles.

xblock_outline_handler (the course outline tree) was already migrated to
the AuthZ-aware user_has_course_permission(..., COURSES_VIEW_COURSE, ...,
LegacyAuthoringPermission.READ) check, so it correctly recognizes AuthZ-
native roles that have no legacy equivalent, like course_auditor and
course_editor.

xblock_container_handler (the unit/container page — what's hit when
navigating to a unit) and xblock_view_handler (renders each child block's
preview fragment on that page), plus xblock_edit_view, were never migrated
the same way: they still called the legacy-only has_studio_read_access
directly, which only recognizes roles with a legacy equivalent
(staff/instructor/limited_staff). A Course Auditor has none, so
get_user_permissions() returned no permissions and these handlers raised
PermissionDenied, even though the outline (using the correct pattern) let
the same user in.

Migrate the three remaining read checks in block.py to the same
user_has_course_permission pattern xblock_outline_handler already uses.
The xblock_handler/handle_xblock CRUD endpoint was already correctly
AuthZ-aware (via _check_xblock_permission) and needed no change.

Fixes openedx/openedx-authz#384
@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Aug 13, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @efortish!

This repository is currently maintained by @openedx/wg-maintenance-openedx-platform-oncall.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

…ctually uses

Manual testing against a real devstack found that the block.py fix alone
wasn't enough: the modern Authoring MFE calls the REST API v1
ContainerHandlerView (/api/contentstore/v1/container_handler/...), which
still 403'd for course_auditor.

That view (and container_handler/container_embed_handler/xblock_edit_view
in the legacy views) all route through the shared _get_item_in_course()
helper in component.py, which gated on has_course_author_access — a
legacy-only *write* check — even though all four callers only need read
access to render a view. course_auditor has no legacy role equivalent, so
it never had write access and always got PermissionDenied here, regardless
of the block.py fix.

Migrate _get_item_in_course() to the same user_has_course_permission read
check, fixing all four callers (including the REST API v1 view) at their
single shared choke point instead of patching each call site.

Verified locally end-to-end: mounted this branch into a real devstack,
assigned course_auditor to a test user, confirmed the unit page 403'd
before this commit and loads correctly after it. Also ran the full
test_block.py + test_vertical_block.py suites against the same devstack
(186 passed).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Needs Triage

Development

Successfully merging this pull request may close these issues.

Course Auditor receives 403 error when navigating to course units

2 participants