Skip to content

Task - Audit Authoring Views for Permission Edge Cases #286

Open

Description

@MaferMazu

Overview

Perform a manual audit of the Authoring flow and key views (Course Overview, Units Overview) to ensure UI stability. We need to verify that the application doesn't crash or break when users have restricted or missing permissions.

Scope of Work

  • Flow Audit: Navigate through the Authoring module using the new granular roles (Editor and Auditor).
  • Critical Views: Verify Course Overview and Units Overview render correctly without permission errors.
  • Error Handling: Ensure the UI handles empty states or restricted data gracefully.
  • Bug Reporting: Document any UI breaks or inconsistent states found during the audit.

Activity

  1. MaferMazu commented on Jun 9, 2026

    @MaferMazu
    ContributorAuthor

    @gviedma-aulasneo will create a requirement list to review this ticket.

  2. moved this from Blocked to In Progress in RBAC AuthZ Boardon Sep 2, 2026
  3. dcoa commented on Sep 2, 2026

    @dcoa
    Contributor

    Findings

    1. Wrap any link to Proctored Exams Settings with courses.manage_pages_and_resources

    Screencast.from.2026-09-02.14-48-12.webm

    From the backend side

    • ProctoredExamSettingsView (GET and POST) is not authz-validated, in this case it should validate courses.manage_pages_and_resources . Only its sibling ProctoringErrorsView is (used for advanced settings page).

    2. Remove the link to the library in a unit when the user does not have permission for that specific library

    When a unit content comes from a library the course outline displays an Info Alert at the top with 2 links

    • Learn more: that redirect to Open edX documentation about Library Updates.
    • library content: Redirect to the library. We can validate whether a user hold permission for the library and allow the redirection or display plain text when is not allowed.
    Screencast.from.2026-09-02.15-43-37.webm

    3. As Curse Editor I cannot tag a unit or content that has not be tagged before

    The drawer makes two calls, and only one of them knows about openedx-authz:

    • GET /api/content_tagging/v1/taxonomies/?org=X returns what you can tag with ❌ no authz aware
    • GET /api/content_tagging/v1/object_tags/{id} returns tags already on the block ✅ authz aware

    get_admin_orgs / get_user_orgs (content_tagging/rules.py) resolve only legacy roles — OrgStaffRole, OrgInstructorRole, OrgContentCreatorRole, OrgLibraryUserRole, and CourseStaffRole/CourseInstructorRole rows in student_courseaccessrole. A user who holds only the authz course_editor role has no such rows, so both lists are empty and the queryset is emptied — including the all-org (org=None) taxonomy, since the early return happens before the Q(org=None) clause is ever reached. Empty list → nothing to add.

    Screencast.from.2026-09-02.16-42-51.webm

    I not sure if that could be the correct behavior. Or whether this overlaps the taxonomy permission, that has not been implemented. Because of this, the following message is displayed and still display the manage button:

    Other tags
    These tags are already applied, but you can't add new ones as you don't have access to their taxonomies.

    4. As Cause Auditor/Course Editor when open the library updates modal the libraries API return 403 if I don't have libraries.view_library permission.

    Related endpoints:

    • /api/libraries/v2/containers/<library_unit_id>/children/?published=true
    • /api/libraries/v2/containers/<library_unit_id>/
    • /xblocks/v2/lb:Demo:L1:html:92963022-f922-460c-8950-f3887591035b/embed/student_view/?version=published
    Screencast.from.2026-09-02.19-14-49.webm
  4. BryanttV commented on Sep 15, 2026

    @BryanttV
    Contributor

    Hi @dcoa, thanks for the detailed report.

    1. Yes, from the frontend we need to validate the permission to determine whether to show the link or not (Wrap Proctored Exams Settings links with courses.manage_pages_and_resources permission #442). From the backend, RBAC permissions will be validated for the proctoring view (GET and POST). In theory, it should also use courses.manage_pages_and_resources (Add authz validation for courses.manage_pages_and_resources to ProctoredExamSettingsView #443).
    2. Yes, we need to make sure the link is only shown to those who have view permissions in the library (content_libraries.view_library) (Remove the link to the library in a unit when the user does not have permission for that specific library #450)
    3. The taxonomies endpoint will be updated to be authz aware (Make TaxonomyOrgView authz-aware to allow access for users with courses.manage_tags permission #448)
    4. We will review the failing endpoints that should allow the review of the changes. A user with the courses.manage_library_updates permission should be able to view the changes even if they don't have view permissions in the library, similar to how it was resolved for the sync endpoint (Fix review endpoints to allow access for users with courses.view_library_updates permission #441).
  5. moved this from In Progress to In Review in RBAC AuthZ Boardon Sep 23, 2026
  6. dcoa commented on Oct 21, 2026

    @dcoa
    Contributor

    Findings

    1. View-only users (e.g. course auditor) can open course-app settings in Pages & Resources through a deep link

    All course-app settings in Pages & Resources are opened through the same route, /pages-and-resources/:appId/settings, which loads the matching @openedx-plugins/course-app-<appId> package from plugins/course-apps. For view-only users (view_pages_and_resources without manage_pages_and_resources) the settings button is hidden, but the route itself has no permission check, so the modal can still be opened by URL.

    How the modal behaves then depends on how the plugin is built:

    • Plugins built on AppSettingsModal (e.g. Progress, Calculator) read isEditable from PagesAndResourcesContext and disable their controls, so the modal opens read-only.
    • Plugins with a custom modal (e.g. ORA, Proctoring, Xpert unit summaries) don't read isEditable, so the modal opens with all options enabled (e.g. /pages-and-resources/ora_settings/settings). Saving fails only because the backend rejects the request.
    Screencast.from.2026-10-21.11-49-04.webm

    Possible solutions:

    1. Guard the settings routes in PagesAndResources (:appId/settings and the Discussions routes) with the manage permission. View-only users are redirected to Pages & Resources and no modal is shown, whichever way the plugin implements its modal.
    Screencast.from.2026-10-21.11-52-17.webm
    1. Complete the validation of isEditable for each custom modal as AppSettingsModal does.

    Caveat: The route guard only covers course apps installed at build time from plugins/course-apps (or other @openedx-plugins/course-app-* packages), this route. Plugins added through a plugin slot (e.g. additional_course_plugin) don't use routes and open their own UI, so they have to check permissions themselves, for example by readidResourcesContext`.

    In addition, custom pages is also unguarded but in their own path /custom-pages we still can disable actions based on manage.pages_and_resources as this one is the only entrance point.

    Image

    2. Course Editor can not save some of the settings in pages and resources

    Teams and ORA settings can't be saved because they only validate advanced_settings this is because there are some settings that are distributed between both views (Advance Settings and Pages and Resources).

    Screencast.from.2026-10-21.16-53-58.webm

    The frontend already disabled the button for teams and wiki

    // PageSettingButton.jsx:54
    const isGearDisabled = (id === 'progress' || id === 'wiki') && !canManageAdvancedSettings;

    We can do the same but that does not solve the deep link, so if we want to keep this 4 apps (ORA, teams, progrss and wiki) manage by advance settings only we need to disabled the buttons directly in the modal or allow at the backed side both permissions for this 4 apps.

    3. Editor can't import YouTube transcripts and gets no error

    Cause: every /transcripts/* view goes through _get_item (views/transcripts_ajax.py:775-783), which uses has_course_author_access. Editors get a 403 on /transcripts/check. The MFE only shows the import card when command == "import" and ignores the error (VideoSourceWidget/hooks.jsx:18-27).

    Screencast.from.2026-10-21.16-11-34.webm

    Fix:

    • Backend: in _get_item, replace the legacy check with user_has_course_permission(..., COURSES_EDIT_COURSE_CONTENT, ..., LegacyAuthoringPermission.WRITE). All of these views serve the video editor, which requires edit permission anyway.
    • MFE (optional): surface an error when checkTranscriptsForImport fails, so a denied request isn't silent.

    4. XBlock (such as Drag and Drop, survey ,etc) studio handler drops the course editor's writes silently

    Cause: component_handler (views/component.py:567) decides whether to persist with the legacy has_course_author_access. Editors have no legacy role (LEGACY_COURSE_ROLE_EQUIVALENCES doesn't include them), so update_item is skipped and only a warning is logged. The handler still returns 200, so the UI reports success.

    Screencast.from.2026-10-21.16-04-03.webm

    Fix:

    if user_has_course_permission(
        request.user, COURSES_EDIT_COURSE_CONTENT.identifier,
        usage_key.course_key, LegacyAuthoringPermission.WRITE,
    ):
        modulestore().update_item(block, request.user.id, asides=asides)

    The else branch should probably return 403 instead of failing quietly, at least when the handler actually changed fields.

    1. Finding: Pages & Resources → Live can't load
      Who: everyone with authz-only roles
      Evidence: course_live/providers and course_live/course → 403
      Cause: course_live/permissions.py IsStaffOrInstructor only accepts legacy GlobalStaff, CourseInstructorRole or CourseStaffRole
      Fix: Use user_has_course_permission: view for GET, MANAGE_PAGES_AND_RESOURCES for writes

    6. Backend only it requires direct API call, so leave them separately from the ones reachable from the UI

    This require a second opinion there are more like security validation at the API level.

    a. Editor can publish by adding metadata to make_public

    Cause: _get_xblock_authz_permission (view_handlers.py:216-229) chooses the permission from the shape of the request body. It only asks for publish_course_content on a "pure" publish. Once the body contains any content field (_CONTENT_FIELDS, :109), it asks for edit_course_content instead. _save_xblock then publishes anyway (:696-702).

    Fix: decide by what the request does. If it will publish, require publish permission as well as edit:

    needs_publish = publish == "make_public" or (
        publish == "republish"
        and block_type not in DIRECT_ONLY_CATEGORIES
        and modulestore().has_published_version(xblock)
    )

    Then check EDIT_COURSE_CONTENT (when there's content), PUBLISH_COURSE_CONTENT (when needs_publish), or both. The simplest way is for _get_xblock_authz_permission to return a list and for _check_xblock_permission to require all of them. Product needs to decide one case: an editor changing visibility on a published unit. Either deny it (403), or save the change as a draft and downgrade republish to no publish.

    b. Duplicating from another course writes into that course

    Cause: there are two bugs.

    • When authz is on, nothing checks access to the source course. The read check on the source only runs on the legacy path (view_handlers.py:362-366).
    • duplicate_block builds the new key from the source location (contentstore/utils.py:1132: source_item.location.replace(name=uuid4().hex)). So the new block is created in the source course and then attached as a child of a parent in another course. That creates a reference across courses and leaves a structure that can't be deleted (KeyError).

    Fix:

    • Reject duplicates across courses: if source_course != parent_usage_key.course_key: raise PermissionDenied (or return 400). Studio only duplicates within a single course, so nothing legitimate breaks.
    • If the product ever wants duplicates across courses, check VIEW_COURSE , and build dest_usage_key in the parent's course.

    c. ContainerChildrenView has no permission check

    Cause: rest_api/v1/views/course_index.py:125 only applies view_auth_classes(is_authenticated=True). Unlike its sibling ContainerHandlerView, it never calls _get_item_in_course, so it never checks course access. This was already the case on upstream m

    Fix: at the top of get():

    if not user_has_course_permission(
        request.user, COURSES_VIEW_COURSE.identifier,
        usage_key.course_key, LegacyAuthoringPermission.READ,
    ):
        self.permission_denied(request)

    This is safe for the UI, because the unit page already requires the same permission through container_handler. Related: get_xblock creates course_info blocks on a GET when they don't exist (view_handlers.py:1140), so the check has to come before that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions