feat: add AuthZ permissions to course creation and outline#38259
feat: add AuthZ permissions to course creation and outline#38259dwong2708 wants to merge 4 commits intoopenedx:masterfrom
Conversation
|
Thanks for the pull request, @dwong2708! This repository is currently maintained by 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 approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo 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:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. 🔘 Update the status of your PRYour PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate. 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:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
0445013 to
03241cc
Compare
1936d74 to
610511d
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds AuthZ (openedx-authz) permission enforcement for course authoring/course-visibility-related endpoints (course detail, outline/course index, downstream summary, and legacy content migration), plus new/updated tests and test utilities to validate the new behavior behind the course-authoring AuthZ feature flag.
Changes:
- Enforce
courses.view_coursechecks (with legacy fallbacks where applicable) for several course authoring/read endpoints. - Add an AuthZ-aware course creation permission helper and integration/unit tests for the updated authorization paths.
- Expand AuthZ test mixins to include staff and superuser clients for repeated use in endpoint tests.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| openedx/core/djangoapps/authz/tests/mixins.py | Extends AuthZ test mixin with staff/superuser users and API clients. |
| lms/djangoapps/courseware/courses.py | Adjusts course access flow when AuthZ authoring is enabled (staff-bypass behavior changes). |
| lms/djangoapps/courseware/access.py | Updates see_about_page access logic to use AuthZ permission checks under the flag. |
| lms/djangoapps/course_api/tests/test_api.py | Adds AuthZ-focused tests for course_detail and related access behavior. |
| common/djangoapps/student/auth.py | Introduces AuthZ-backed is_content_creator check for course creation. |
| cms/djangoapps/contentstore/tests/test_course_create_rerun.py | Adds AuthZ integration tests for course creation via course_handler. |
| cms/djangoapps/contentstore/rest_api/v2/views/downstreams.py | Enforces courses.view_course AuthZ permission for downstream summary endpoint. |
| cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py | Adds AuthZ tests for downstream summary endpoint access control. |
| cms/djangoapps/contentstore/rest_api/v1/views/course_index.py | Enforces courses.view_course AuthZ permission for course outline (course index) endpoint. |
| cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py | Adds AuthZ tests for course index access control. |
| cms/djangoapps/contentstore/api/views/course_validation.py | Moves migrate-legacy-content list endpoint to AuthZ permission decorator and adds DeveloperError mixin. |
| cms/djangoapps/contentstore/api/tests/test_validation.py | Updates/extends tests for migrate-legacy-content endpoint authorization under AuthZ. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # If AuthZ is enabled for this course, it checks already | ||
| # permissions for staff. | ||
| return non_staff_access_response |
There was a problem hiding this comment.
The AuthZ toggle branch has inconsistent indentation (extra leading space) compared to the surrounding block. This makes the code harder to read and may trip style/lint checks; align the comment and return indentation with the if core_toggles... block’s standard 4-space indentation level.
| # If AuthZ is enabled for this course, it checks already | |
| # permissions for staff. | |
| return non_staff_access_response | |
| # If AuthZ is enabled for this course, it checks already | |
| # permissions for staff. | |
| return non_staff_access_response |
| """ | ||
| if user and not user.is_anonymous and core_toggles.enable_authz_course_authoring(courselike.id): | ||
| is_authz_allowed = user_has_course_permission(user, COURSES_VIEW_COURSE.identifier, courselike.id) | ||
| return ACCESS_GRANTED if is_authz_allowed else ACCESS_DENIED |
There was a problem hiding this comment.
When AuthZ is enabled, can_see_about_page returns a plain ACCESS_DENIED for unauthorized users instead of returning the typed CatalogVisibilityError that the legacy path returns. This removes the ability for downstream handlers to show the existing meaningful “not visible in catalog” error message for logged-in users under the flag.
| return ACCESS_GRANTED if is_authz_allowed else ACCESS_DENIED | |
| if is_authz_allowed: | |
| return ACCESS_GRANTED | |
| # Preserve the legacy typed CatalogVisibilityError response for denied users | |
| # so downstream handlers can continue showing the existing meaningful message. | |
| return legacy_can_see_about_page() |
| if core_toggles.AUTHZ_COURSE_AUTHORING_FLAG.is_enabled(): | ||
| return _has_content_creator_access(user, org) | ||
| return _has_legacy_content_creator_access(user, org) | ||
|
|
||
|
|
||
| def _has_content_creator_access(user, org): | ||
| if settings.FEATURES.get('DISABLE_COURSE_CREATION', False): | ||
| return False | ||
| org_scope_key = f"course-v1:{org}+*" | ||
| return authz_api.is_user_allowed( | ||
| user.username, | ||
| COURSES_EDIT_COURSE_CONTENT.identifier, | ||
| org_scope_key | ||
| ) |
There was a problem hiding this comment.
This AuthZ branch for course creation does not preserve the legacy GlobalStaff/staff bypass: user_has_role grants Django staff users access, but _has_content_creator_access only checks authz_api.is_user_allowed(...). If AuthZ is enabled without explicit role assignment, Django staff/superusers may lose the ability to create/rerun courses.
| def _has_content_creator_access(user, org): | ||
| if settings.FEATURES.get('DISABLE_COURSE_CREATION', False): | ||
| return False | ||
| org_scope_key = f"course-v1:{org}+*" | ||
| return authz_api.is_user_allowed( | ||
| user.username, | ||
| COURSES_EDIT_COURSE_CONTENT.identifier, | ||
| org_scope_key | ||
| ) |
There was a problem hiding this comment.
_has_content_creator_access is used for course creation but checks COURSES_EDIT_COURSE_CONTENT. This appears inconsistent with the PR’s stated permission model (courses.create_course) and makes it unclear which permission is actually required to create a course. Consider using the dedicated create-course permission constant (or renaming the helper to reflect that it is checking edit-content permission).
| @override_settings(FEATURES={"DISABLE_COURSE_CREATION": False}) | ||
| def test_create_course_staff(self): | ||
| """ | ||
| Staff user can create course. | ||
| """ | ||
| response = self.authorized_staff_client.ajax_post(self.url, { | ||
| "org": self.org, | ||
| "number": "CS101", | ||
| "display_name": "Authz Course", | ||
| "run": "2026_T1", | ||
| }) | ||
|
|
||
| # At the moment of implement new permissions for course creation, | ||
| # the staff user has no role and thus is unauthorized. | ||
| self.assertEqual(response.status_code, 403) | ||
|
|
There was a problem hiding this comment.
The test docstring says “Staff user can create course”, but the assertion expects a 403. Either the expected behavior or the docstring/test name should be updated so the test intent matches the assertion (and aligns with the rest of the AuthZ tests that treat staff/superusers as elevated users).
| cls.authorized_user = cls.create_user('authorized', is_staff=False) | ||
| cls.unauthorized_user = cls.create_user('unauthorized', is_staff=False) | ||
| cls.staff_user = cls.create_user('staff', is_staff=True) | ||
|
|
There was a problem hiding this comment.
CourseAuthoringAuthzTestMixin.setUp() already creates self.authorized_user, self.unauthorized_user, and self.staff_user for each test. The additional user objects created in this class’s setUpClass() are unused (they’re overwritten in setUp()), which adds unnecessary DB setup and makes it harder to understand which users are actually used in assertions.
| cls.authorized_user = cls.create_user('authorized', is_staff=False) | |
| cls.unauthorized_user = cls.create_user('unauthorized', is_staff=False) | |
| cls.staff_user = cls.create_user('staff', is_staff=True) |
Description
Resolves: #38129
This PR introduces new AuthZ permissions for the course list and related course endpoints, aligning them with the
course-v1:*permission model.It ensures that course visibility and creation are consistently enforced using the new authorization system.
Permissions Implemented
The following endpoints were updated to use AuthZ permissions:
Course creation
POST /course/ → courses.create_courseUses Org-scoped authorization, since the course does not yet exist at creation time.
Course read / visibility
The following endpoints now require courses.view_course:
GET /api/courses/v1/courses/<course_id>GET /api/contentstore/v1/course_index/<course_id>GET /api/contentstore/v2/downstreams/<course_id>/summaryGET /api/courses/v1/migrate_legacy_content_blocks/<course_id>Excluded Endpoint
The following endpoint was intentionally not updated:
GET /api/content_tagging/v1/object_tag_counts/<course_id>/Reason:
Testing instructions
Added/updated tests to validate:
Deadline
Verawood