From a72ba7ad968c0d7d46d16038b804a5ab9b26a146 Mon Sep 17 00:00:00 2001 From: Fox Danger Piacenti Date: Wed, 2 Sep 2026 17:48:31 -0500 Subject: [PATCH 1/3] feat: libraries v2 support for studio perms --- common/djangoapps/student/auth.py | 33 +++++++++++++++++-- .../core/djangoapps/xblock/runtime/runtime.py | 3 ++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/student/auth.py b/common/djangoapps/student/auth.py index cb617f367096..055004415d57 100644 --- a/common/djangoapps/student/auth.py +++ b/common/djangoapps/student/auth.py @@ -9,7 +9,7 @@ from ccx_keys.locator import CCXBlockUsageLocator, CCXLocator from django.conf import settings from django.core.exceptions import PermissionDenied -from opaque_keys.edx.locator import LibraryLocator +from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2 from openedx_authz import api as authz_api from openedx_authz.constants.permissions import ( COURSES_MANAGE_ADVANCED_SETTINGS, @@ -32,6 +32,7 @@ strict_role_checking, ) from openedx.core import toggles as core_toggles +from openedx.core.types.user import User as UserType # Studio permissions: STUDIO_EDIT_ROLES = 8 @@ -80,6 +81,21 @@ def user_has_role(user, role): return False +def lib_perm_check_to_bool( + library_key: LibraryLocatorV2, user: UserType, permission: str | authz_api.data.PermissionData +): + """ + Returns True if the API call to library permissions does not raise, + and False otherwise. + """ + from openedx.core.djangoapps.content_libraries.api import require_permission_for_library_key + try: + require_permission_for_library_key(library_key, user, permission) + return True + except PermissionError: + return False + + def get_user_permissions(user, course_key, org=None, service_variant=None): """ Get the bitmask of permissions that this user has in the given course context. @@ -92,6 +108,8 @@ def get_user_permissions(user, course_key, org=None, service_variant=None): :param service_variant: the variant of the service (lms or cms). Permissions may differ between the two, see the HACK comment in the function for more details. """ + from openedx.core.djangoapps.content_libraries.permissions import CAN_VIEW_THIS_CONTENT_LIBRARY, \ + CAN_EDIT_THIS_CONTENT_LIBRARY if org is None: org = course_key.org course_key = course_key.for_branch(None) @@ -126,7 +144,17 @@ def get_user_permissions(user, course_key, org=None, service_variant=None): if OrgStaffRole(org=org).has_user(user) or (course_key and user_has_role(user, CourseStaffRole(course_key))): return STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT - # Otherwise, for libraries, users can view only: + # For libraries v2, permissions can be specific to the library + if course_key and isinstance(course_key, LibraryLocatorV2): + read_value = STUDIO_NO_PERMISSIONS + write_value = STUDIO_NO_PERMISSIONS + if lib_perm_check_to_bool(course_key, user, CAN_VIEW_THIS_CONTENT_LIBRARY): + read_value = STUDIO_VIEW_CONTENT + if lib_perm_check_to_bool(course_key, user, CAN_EDIT_THIS_CONTENT_LIBRARY): + write_value = STUDIO_EDIT_CONTENT + return read_value | write_value + + # Otherwise, for legacy libraries, users can view only: if course_key and isinstance(course_key, LibraryLocator): if OrgLibraryUserRole(org=org).has_user(user) or user_has_role(user, LibraryUserRole(course_key)): return STUDIO_VIEW_USERS | STUDIO_VIEW_CONTENT @@ -180,6 +208,7 @@ def has_studio_read_access(user, course_key): There is currently no such thing as read-only course access in studio, but there is read-only access to content libraries. """ + print("STUDIO_VIEW_CONTENT is ", STUDIO_VIEW_CONTENT) return bool(STUDIO_VIEW_CONTENT & get_user_permissions(user, course_key)) diff --git a/openedx/core/djangoapps/xblock/runtime/runtime.py b/openedx/core/djangoapps/xblock/runtime/runtime.py index f3544a6102d8..372e8e8275e3 100644 --- a/openedx/core/djangoapps/xblock/runtime/runtime.py +++ b/openedx/core/djangoapps/xblock/runtime/runtime.py @@ -355,6 +355,9 @@ def service(self, block: XBlock, service_name: str): return DiscussionConfigService() elif service_name == 'xqueue': return XQueueService(block) + elif service_name == 'studio_user_permissions': + from cms.djangoapps.contentstore.utils import StudioPermissionsService + return StudioPermissionsService(self.user) # Otherwise, fall back to the base implementation which loads services # defined in the constructor: From 82f1b50b34cb4ac3964316c4aded0de32d152437 Mon Sep 17 00:00:00 2001 From: Fox Danger Piacenti Date: Wed, 2 Sep 2026 18:04:02 -0500 Subject: [PATCH 2/3] refactor: move StudioPermissionsService --- cms/djangoapps/contentstore/utils.py | 24 ++--------------- cms/djangoapps/contentstore/views/preview.py | 3 ++- openedx/core/djangoapps/content/services.py | 27 +++++++++++++++++++ .../core/djangoapps/xblock/runtime/runtime.py | 2 +- 4 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 openedx/core/djangoapps/content/services.py diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 143f01cde32a..99a628a148f1 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -53,7 +53,7 @@ from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.edxmako.services import MakoService from common.djangoapps.student import auth -from common.djangoapps.student.auth import STUDIO_EDIT_ROLES, has_studio_read_access, has_studio_write_access +from common.djangoapps.student.auth import STUDIO_EDIT_ROLES, has_studio_write_access from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff from common.djangoapps.track import contexts @@ -71,6 +71,7 @@ from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from openedx.core import toggles as core_toggles from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.content.services import StudioPermissionsService from openedx.core.djangoapps.content_libraries.api import get_container from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course @@ -2243,27 +2244,6 @@ def get_group_configurations_context(course, store): return context -class StudioPermissionsService: - """ - Service that can provide information about a user's permissions. - - Deprecated. To be replaced by a more general authorization service. - - Only used by LegacyLibraryContentBlock (and library_tools.py). - """ - - def __init__(self, user): - self._user = user - - def can_read(self, course_key): - """ Does the user have read access to the given course/library? """ - return has_studio_read_access(self._user, course_key) - - def can_write(self, course_key): - """ Does the user have read access to the given course/library? """ - return has_studio_write_access(self._user, course_key) - - def track_course_update_event(course_key, user, course_update_content=None): """ Track course update event diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 408c5649c8d5..86199712031f 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -44,7 +44,8 @@ from xmodule.util.sandboxing import SandboxService from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, XModuleMixin -from ..utils import StudioPermissionsService, get_visibility_partition_info +from ..utils import get_visibility_partition_info +from openedx.core.djangoapps.content.services import StudioPermissionsService from .access import get_user_role from .session_kv_store import SessionKeyValueStore diff --git a/openedx/core/djangoapps/content/services.py b/openedx/core/djangoapps/content/services.py new file mode 100644 index 000000000000..4bb000940f3d --- /dev/null +++ b/openedx/core/djangoapps/content/services.py @@ -0,0 +1,27 @@ +""" +Services for learning content +""" +from __future__ import annotations + +from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access + + +class StudioPermissionsService: + """ + Service that can provide information about a user's permissions. + + Deprecated. To be replaced by a more general authorization service. + + Only used by LegacyLibraryContentBlock (and library_tools.py). + """ + + def __init__(self, user): + self._user = user + + def can_read(self, course_key): + """ Does the user have read access to the given course/library? """ + return has_studio_read_access(self._user, course_key) + + def can_write(self, course_key): + """ Does the user have read access to the given course/library? """ + return has_studio_write_access(self._user, course_key) diff --git a/openedx/core/djangoapps/xblock/runtime/runtime.py b/openedx/core/djangoapps/xblock/runtime/runtime.py index 372e8e8275e3..dd4b8a644fb8 100644 --- a/openedx/core/djangoapps/xblock/runtime/runtime.py +++ b/openedx/core/djangoapps/xblock/runtime/runtime.py @@ -356,7 +356,7 @@ def service(self, block: XBlock, service_name: str): elif service_name == 'xqueue': return XQueueService(block) elif service_name == 'studio_user_permissions': - from cms.djangoapps.contentstore.utils import StudioPermissionsService + from openedx.core.djangoapps.content.services import StudioPermissionsService return StudioPermissionsService(self.user) # Otherwise, fall back to the base implementation which loads services From 4f0ad6368ad9e39b2e927e6b2459afcaa45e4faf Mon Sep 17 00:00:00 2001 From: Fox Danger Piacenti Date: Wed, 2 Sep 2026 18:10:46 -0500 Subject: [PATCH 3/3] refactor: sort imports --- cms/djangoapps/contentstore/views/preview.py | 2 +- common/djangoapps/student/auth.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 86199712031f..8c518fd4c605 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -29,6 +29,7 @@ from common.djangoapps.student.models import anonymous_id_for_user from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from lms.djangoapps.lms_xblock.field_data import LmsFieldData +from openedx.core.djangoapps.content.services import StudioPermissionsService from openedx.core.djangoapps.discussions.services import DiscussionConfigService from openedx.core.djangoapps.video_config.services import VideoConfigService from openedx.core.lib.cache_utils import CacheService @@ -45,7 +46,6 @@ from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, XModuleMixin from ..utils import get_visibility_partition_info -from openedx.core.djangoapps.content.services import StudioPermissionsService from .access import get_user_role from .session_kv_store import SessionKeyValueStore diff --git a/common/djangoapps/student/auth.py b/common/djangoapps/student/auth.py index 055004415d57..aa64dd07d9c1 100644 --- a/common/djangoapps/student/auth.py +++ b/common/djangoapps/student/auth.py @@ -108,8 +108,10 @@ def get_user_permissions(user, course_key, org=None, service_variant=None): :param service_variant: the variant of the service (lms or cms). Permissions may differ between the two, see the HACK comment in the function for more details. """ - from openedx.core.djangoapps.content_libraries.permissions import CAN_VIEW_THIS_CONTENT_LIBRARY, \ - CAN_EDIT_THIS_CONTENT_LIBRARY + from openedx.core.djangoapps.content_libraries.permissions import ( + CAN_EDIT_THIS_CONTENT_LIBRARY, + CAN_VIEW_THIS_CONTENT_LIBRARY, + ) if org is None: org = course_key.org course_key = course_key.for_branch(None)