Skip to content

Conversation

@ccantillo
Copy link
Collaborator

Description

This change enforces course-level role restrictions for content publishing in Studio, preventing users with only the "staff" role from publishing content, regardless of their GlobalStaff status.

Impact: Course Authors (Staff role specifically)

Previously, GlobalStaff users could bypass course-level permissions. Now, if a user is assigned the "staff" role (not "instructor") in a course, they cannot publish content even if they have GlobalStaff privileges. This ensures course-level role assignments take precedence over global permissions.

User Roles Affected:

  • Course Staff: No longer able to publish content (intended behavior)
  • Course Instructors: Can still publish content (no change)

Files Modified:

  • cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py - Updated handle_xblock() permission logic

Supporting information

Related to course permission management and role-based access control in Open edX Studio.

Testing instructions

  1. Create a test course
  2. Assign a GlobalStaff user the "staff" role (not "instructor") in the course
  3. Log in as that user and navigate to a unit in Studio
  4. Attempt to publish the unit
  5. Expected: User receives 403 error: "Only instructors can publish content. Staff members do not have publish permissions."
  6. Verify that users with "instructor" role can still publish successfully

@ccantillo ccantillo requested a review from johanseto January 2, 2026 13:14
)
else:
log.info(f"Publish ALLOWED for user: {request.user.username}, roles={user_course_roles}, global_staff={is_global_staff}")
except Exception as e:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broad exception

log.info(f"request.json exists: {hasattr(request, 'json')}, request.json value: {getattr(request, 'json', None)}")

# Check if user is trying to publish and if they have permission
if request.method in ("POST", "PUT", "PATCH"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why GET not??

user_course_roles = list(CourseAccessRole.objects.filter(
user=request.user,
course_id=usage_key.course_key,
role__in=['instructor', 'staff']
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
role__in=['instructor', 'staff']
role__in=['instructor', 'staff'],

log.warning(f"Publish DENIED for staff-only user: {request.user.username} (global_staff={is_global_staff})")
return JsonResponse(
{
"error": _("Only instructors can publish content. Staff members do not have publish permissions.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using _?

# First, check for course-specific role
role_entry = CourseAccessRole.objects.filter(
user=user,
course_id=course_key
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
course_id=course_key
course_id=course_key,

role_entry = CourseAccessRole.objects.filter(
user=user,
org=course_key.org,
course_id=CourseKeyField.Empty
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
course_id=CourseKeyField.Empty
course_id=CourseKeyField.Empty,

re_path(
fr'^course_user_role/{COURSE_ID_PATTERN}$',
CourseUserRoleView.as_view(),
name="course_user_role"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name="course_user_role"
name="course_user_role",

# Check if user is trying to publish and if they have permission
if request.method in ("POST", "PUT", "PATCH"):
try:
publish_action = request.json.get("publish") if hasattr(request, 'json') and request.json else None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
publish_action = request.json.get("publish") if hasattr(request, 'json') and request.json else None
publish_action = request.json.get("publish") if hasattr(request, "json") and request.json else None

single or double quotes

from .course_index import CourseIndexSerializer
from .course_rerun import CourseRerunSerializer
from .course_team import CourseTeamSerializer
from .course_user_role import CourseUserRoleSerializer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CourseUserRoleSerializer Do you need it?

from .course_rerun import CourseRerunView
from .course_waffle_flags import CourseWaffleFlagsView
from .course_team import CourseTeamView
from .course_user_role import CourseUserRoleView
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you use it? CourseUserRoleView

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.

3 participants