Skip to content

grant xblock edit access via authz edit_course_content permission - #39050

Draft
jacobo-dominguez-wgu wants to merge 16 commits into
openedx:masterfrom
WGU-Open-edX:course-editor-content
Draft

grant xblock edit access via authz edit_course_content permission#39050
jacobo-dominguez-wgu wants to merge 16 commits into
openedx:masterfrom
WGU-Open-edX:course-editor-content

Conversation

@jacobo-dominguez-wgu

Copy link
Copy Markdown
Contributor

Description

In Studio, the per-component "Edit" action on an XBlock card is controlled by the can_edit flag in xblock_view_handler. Previously can_edit was derived solely from the legacy has_studio_write_access check.

Under the authz course-authoring rollout, a user can be granted courses.edit_course_content without holding legacy studio write access. Those users were incorrectly denied the "Edit" action on component cards, even though the authz
permission is meant to grant exactly that capability.

This change makes can_edit the union of the legacy check and the authz path:

can_edit = has_studio_write_access(request.user, usage_key.course_key) or (
    is_authz_authoring_enabled and authz_can_edit_course_content
)

The is_authz_authoring_enabled / authz_can_edit_course_content flags are already resolved for this request via _get_authz_permissions_flags, so no additional permission lookups are introduced. When the authz flag is off, behavior is unchanged.

Supporting information

Fixes openedx/openedx-authz#418

Testing instructions

Automated:

  • Run the regression tests added in TestXBlockViewHandlerHeaderActionsAuthz:

    DJANGO_SETTINGS_MODULE=cms.envs.test uv run python -m pytest \
      "cms/djangoapps/contentstore/views/tests/test_block.py::TestXBlockViewHandlerHeaderActionsAuthz" -p no:randomly
    

    They cover the component edit button on a leaf html component preview:

    • authz flag on + edit_course_content granted + no legacy write access → edit button shown
    • authz flag on + permission denied + no legacy write access → edit button hidden

Manual:

  1. Enable the authz course-authoring flag for a course.
  2. Grant a user courses.edit_course_content without legacy studio write access.
  3. Open a unit in Studio and confirm the component cards show the "Edit" action.
  4. Revoke the permission and confirm the "Edit" action disappears.

Demo

User with edit_course_content permission must be able to edit course content.

Screen.Recording.2026-08-31.at.3.16.16.PM.mov

AI usage notice

Used Copilot with auto model mode (Claude) to assist on the modification and creation of unit tests.

Important

Needs to be reviewed after #39013, it contains code from that pr.

@openedx-webhooks openedx-webhooks added open-source-contribution PR author is not from Axim or 2U core contributor PR author is a Core Contributor (who may or may not have write access to this repo). labels Aug 31, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @jacobo-dominguez-wgu!

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.

🔘 Update the status of your PR

Your 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:

  • 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.

ChrisChV and others added 10 commits August 31, 2026 18:47
…all courses or specific courses (openedx#38676)

Add a management command for generating a csv report of xblocks used in courses

---------

Co-authored-by: Samuel Allan <samuel@opencraft.com>
Add optional start_date_on_or_after/start_date_on_or_before query params
to CourseOverview.get_all_courses(), threaded through
get_courses_accessible_to_user() and documented on HomePageCoursesViewV2.

Adds get_date_param() (validates the date params, 400 on bad input) and
a db_index on CourseOverview.start.

See openedx/openedx-core#669

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add openedx_learning to INSTALLED_APPS in both lms/envs/common.py and
cms/envs/common.py, so the CompetencyTaxonomy model and its migrations
are active.

Add openedx_learning to the isolated_apps import-linter contract,
restricting other code to importing only its api/models_api/data
modules, and to root_packages, which import-linter needs to build a
graph node for the app at all.

CompetencyTaxonomy already carries a `.. no_pii:` annotation in its
own docstring, so no PII safe-list entry is needed here.

The openedx-core requirements pin (1.3.0) already contains the app;
no requirements change needed.

See openedx#38958

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: add ADR for standardizing REST API URL structure

Open edX REST URLs follow no consistent pattern. The /api/ prefix, the position
of the version, pluralisation, word separators, and trailing slashes all vary,
in places between adjacent lines of a single URLconf. The FC-0118 ADRs
(0025-0037) standardize what happens inside an endpoint but never its address.

Add docs/decisions/0038-standardize-rest-api-url-structure.rst as an accepted
ADR defining twelve rules: a leading /api/ prefix, singular API names with
plural collections, domain-based rather than app-based naming, lowercase
snake_case segments, mounts that declare their own prefix, exact-match routes
with a required trailing slash, version position and form, hierarchy capped at
one level of nesting, opaque-key identifiers resolved by shared path
converters, verb-free resource paths, snake_case Django URL names, and a single
URL namespace shared by the LMS and Studio. Existing endpoints migrate under
OEP-21 with the conforming path mounted alongside the legacy one.

This restates the still-applicable rules from the Open edX REST API Conventions
wiki that OEP-49 defers to, settles what that page left as TBD, and adds a CI
conformance check so the convention is enforced rather than remembered.

Findings recorded in the ADR:

- /api/courses/ is mounted in both services on unrelated implementations, both
  at v1, which blocks the endpoint-by-endpoint combined headless LMS+CMS
  migration.
- /api/enrollment/v1/enrollment and /api/enrollment/v1/enrollments/ are
  different views, so pluralisation is load-bearing today.
- Deprecated Org/Course/Run course keys contain slashes, so nesting one
  mid-path requires a shared path converter; the platform has three, in two
  apps, none reusable.
- Django resolves re_path with re.search, so unanchored patterns in
  course_experience and learner_home match under arbitrary prefixes.

* docs: address review feedback on the URL structure ADR

Four review comments on PR openedx#39003:

- Fix the RST formatting in rule 11. RST does not nest inline markup, so
  **``snake_case``**, ... rendered with visible asterisks and backticks. Rule 11
  now keeps the literal outside the bold lead-in. The same construct in the
  kebab-case rejected-alternative bullet is fixed the same way.

- Stop requiring slash-tolerant course keys. openedx-platform#31134 removed Old
  Mongo create and update operations, leaving only read-only access to static
  assets and the root CourseBlock, so no new deprecated-key course can be
  authored. New and migrated APIs now accept non-deprecated keys only, which
  covers course-v1: and ccx-v1: and refuses Org/Course/Run. The converter regex
  becomes a plain "no slash" match with the check made on course_key.deprecated,
  and mid-path nesting is unambiguous as a result. The now-invalid "breaks on
  keys containing /" clause is dropped from the deep-nesting rejected
  alternative. Only endpoints still serving pre-existing Old Mongo courses need
  the slash-tolerant pattern.

- Record that content_staging, olx_rest_api, content_libraries, and
  lms.djangoapps.instructor are core apps rather than optional extensions. All
  four are absent from the static INSTALLED_APPS lists and arrive through
  get_plugin_apps(), so the ADR recommends moving them into INSTALLED_APPS and
  mounting them explicitly under their own prefix, which is what rule 5 asks of
  any API. The conformance check still walks the composed resolver, because
  third-party plugins will always contribute routes the project URLconfs cannot
  show.

- Answer where the path converters belong: they are generic and need only
  edx-opaque-keys, so they go in edx-drf-extensions alongside the pagination and
  JWT classes, with openedx/core/lib shown as the interim home.

* docs: point ADR 0038 at the shipped edx-drf-extensions converters

The shared opaque-key path converters now live in edx-drf-extensions
(edx_rest_framework_extensions/url_converters.py, registered via
register_url_converters()), so drop the paragraph deferring that
extraction and update the code example and Implementation Notes to
reference the library instead of the interim openedx/core/lib/ path.
…nedx#39024)

* fix: recover from orphaned Mongo course index on course creation

The split modulestore course index is read from MySQL but written to both
MySQL and Mongo's active_versions. Mongo writes are not covered by
ATOMIC_REQUESTS, so if a request creates a course and then fails, the MySQL
row is rolled back while the Mongo doc survives.

That course key is then permanently unusable: has_course() reads MySQL and
reports the course as absent, so creation is retried, and the follower write
to Mongo raises DuplicateKeyError against UNIQUE(org, course, run). Every
retry fails identically, and the rollback leaves no MySQL trace of why.

Clear any stale Mongo doc before the follower write. This is safe because it
runs after new_index.save() has succeeded, which means no MySQL row existed
for the key, which means any Mongo doc for it is stale.

* fix: only clean up the stale Mongo doc when the insert conflicts

Deleting unconditionally added a Mongo round-trip to every course and library
creation, which broke check_mongo_calls in
TestLibraries::test_create_library (expected 3 calls, 4 were made).

Insert first and clean up only on DuplicateKeyError, so the happy path keeps
its original call count and the extra work happens only in the rare case
where a stale doc is actually present.

---------

Co-authored-by: Peter Pinch <pdpinch@mit.edu>
…on-requirements-394de1f

chore: Upgrade Python requirements
…e-country-database-33466049745

Update GeoLite Database
* feat: update tpa config

* fix: silent TPA error messages in MFE account

* feat: pass provider name as dynamic error_code param in account redirect

* feat: surface TPA errors to Account MFE via endpoint instead of query param

* fix: preserve unrelated Django messages in TPA error endpoint

Reading django.contrib.messages marks the whole session storage as
consumed, not just the message we want. Without re-queuing the rest,
any unrelated message queued in the same session (from some other,
unrelated flow) was being silently dropped instead of shown wherever
it was actually meant to appear. Now only the first social-auth
message is consumed; everything else is re-queued.

Also add a drf_yasg schema to the endpoint, matching the convention
already used elsewhere in this app, and expand test coverage: message
preservation, multiple queued social-auth messages, unrecognized
exceptions, and the full set of TPA exception types for both the
redirect-dispatch and message-queuing paths.

* fix: address pylint failures in new tests

- use-implicit-booleaness-not-comparison: "list(...) == []" -> "not list(...)"
- comparison-with-callable: match.func == view_function needs an explicit
  disable, same pattern already used elsewhere in this codebase (e.g.
  common/djangoapps/util/date_utils.py).

* fix: limit test account path only to lms
@mphilbrick211 mphilbrick211 added the mao-onboarding Reviewing this will help onboard devs from an Axim mission-aligned organization (MAO). label Sep 2, 2026
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Sep 2, 2026
efortish and others added 4 commits September 3, 2026 14:35
…signments (openedx#38984)

* fix: RoleCache legacy compat layer ignores platform-wide glob role assignments

authz_get_all_course_assignments_for_user() only fetched CourseOverviewData
and OrgCourseOverviewGlobData scopes, never PlatformCourseOverviewGlobData
(course-v1:*). Even if it had, _get_org_and_course_id_from_authz_scope()
had no branch for it, since a platform-wide scope doesn't map to a single
org the way course/org-wide scopes do.

These assignments feed RoleCache/BulkRoleCache, which back has_access()/
CourseRole.has_user() legacy checks. A user whose only role assignment was
a platform-wide glob got an empty RoleCache for every course, silently
denying access despite the AuthZ assignment existing.

Fix: a platform-wide grant applies to every org, so represent it as an
org-wide grant (org, course_id=None) repeated for every registered org
(reusing organizations.api.get_organizations(), the same helper used for
the analogous fix in openedx-authz#380). This is exactly the shape an
org-wide grant already produces, so it's picked up by the existing
OrgRole-based legacy checks (has_staff_roles, get_user_permissions, which
already check org-level and course-level access separately) with no
changes needed to RoleCache/OrgRole/CourseRole.

Fixes openedx/openedx-authz#379

* refactor: use plain assert instead of self.assertTrue in new tests

Per review feedback from @BryanttV on openedx#38984.

* refactor: use build_external_key() instead of hardcoded 'course-v1:*'

Per review feedback from @BryanttV on openedx#38984.

* refactor: use real role assignment in platform-glob tests, drop dead org check

Per mariajgrimaldi's review: the two platform-glob tests built a
RoleAssignmentData by hand and mocked get_user_role_assignments instead of
exercising the real assignment and policy load path, so switched both to
assign_role_to_user_in_scope + AuthzEnforcer.get_enforcer().load_policy(),
same pattern already used elsewhere in this file.

Also dropped the `scope.org is not None` check in
_get_orgs_and_course_ids_from_authz_scope. An OrgCourseOverviewGlobData
instance can only come from the ScopeData factory, which validates the org
name before selecting that class, so org can't actually be None there.
openedx#39013)

* feat: adding authz edit permission check on xblock wrapper
* feat: adding manage tags check
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core contributor PR author is a Core Contributor (who may or may not have write access to this repo). mao-onboarding Reviewing this will help onboard devs from an Axim mission-aligned organization (MAO). open-source-contribution PR author is not from Axim or 2U

Projects

Status: Waiting on Author

Development

Successfully merging this pull request may close these issues.

Course Editor is not allowed to edit the component's content just the title