Skip to content

fix: Org-level override blocks course creation for legacy creators - #39265

Open
bmtcril wants to merge 1 commit into
masterfrom
bmtcril/fix-rbac-015
Open

bmtcril wants to merge 1 commit into
masterfrom
bmtcril/fix-rbac-015

Conversation

@bmtcril

@bmtcril bmtcril commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Description

With authz.enable_course_authoring on for an organization, a course creator who already holds an organization-wide course_admin gets a 403 from course creation, after the course has been written. Org-wide course_admin is what the AuthZ migration makes from a legacy organization-wide instructor role. Because add_instructor raises partway through create_new_course, the course is left half set up: no enrollment for the creator, no forum role seeding, and the rest of the post-creation steps are skipped.

Fixes openedx/openedx-authz#521.

Cause

add_instructor adds the creator to CourseInstructorRole and then calls auth.add_users(..., CourseStaffRole(...)), whose _check_caller_authority requires the caller to hold the course's instructor role.

Under AuthZ, CourseInstructorRole.add_users goes through authz_add_role, which first asked get_user_role_assignments_in_scope(<new course>) whether the user already had course_admin there. That call also reports roles inherited from org-wide (course-v1:ORG+*) and platform-wide assignments, so for an org-wide admin it said yes, and authz_add_role returned without making a course-level assignment.

The legacy compatibility layer (RoleCache) maps an org-wide assignment to an org-level role with no course key, and CourseRole.has_user only looks at course-level roles. So user_has_role(creator, CourseInstructorRole(course)) was False, and _check_caller_authority raised PermissionDenied.

Measured on a Tutor main sandbox (openedx-authz 1.24.0):

org-wide assignment:     user^u => [role^course_admin] @ course-v1^course-v1:ORG+*
in-scope for new course: user^u => [role^course_admin] @ course-v1^course-v1:ORG+P1+R1
after add_users:         user^u => [role^course_admin] @ course-v1^course-v1:ORG+*   (no course-level row)
has instructor:          False
add_instructor:          django.core.exceptions.PermissionDenied

A creator without an org-wide role is not affected: the pre-check finds nothing and the course assignment is made.

Fix

authz_add_role now assigns the role in the course's own scope directly and drops the inheritance-blind pre-check. assign_role_to_user_in_scope already returns False when that exact assignment exists, so the "already has it" case still returns early (and still emits no duplicate event).

The creator ends up with both the org-wide and a course-level course_admin. That matches legacy behavior, where the creator always got an explicit course role, and it means the creator keeps admin on a course they created if the org-wide role is later revoked.

Testing instructions

  1. Enable authz.enable_course_authoring for an organization (waffle flag org override).
  2. Give a non-staff course creator an org-wide course_admin in that organization (course-v1:<ORG>+*), for example by migrating a legacy org-wide instructor.
  3. As that user, create a course in the organization from Studio.
    • Before: 403 {"error": "User does not have the permission to create courses in this organization or course creation is disabled"}; the course exists but the creator is not enrolled.
    • After: 200, and the creator is the course's instructor and staff.

Other information

Found by the Open edX end-to-end test suite: TC-00640, finding RBAC-015 in docs/findings.md, tracked as end-to-end-tests#118.

🤖 Generated with Claude Opus 5.5.

This branch has not been deployed

No deployments
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.

Course creation returns 403 after creating the course (add_instructor PermissionDenied)

1 participant