Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion common/djangoapps/student/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
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 import permissions as authz_permissions
from openedx_authz.constants.permissions import COURSES_MANAGE_ADVANCED_SETTINGS

from openedx.core import toggles as core_toggles
Expand Down Expand Up @@ -180,6 +181,32 @@ def has_studio_read_access(user, course_key):
return bool(STUDIO_VIEW_CONTENT & get_user_permissions(user, course_key))


def has_library_tagging_access(user, library_key: LibraryLocatorV2) -> bool:
"""
Check if user has permission to tag content in the specified library.

Note: `MANAGE_LIBRARY_TAGS` implies `EDIT_LIBRARY_CONTENT` via authz policies (g2),
so users with tagging permission also have edit permission automatically.

Args:
user (User): Django user object
library_key (LibraryLocatorV2): Key for the library

Returns:
bool: True if user has permission to tag content in the library, False otherwise
"""
# Import here to avoid circular import
from openedx.core.djangoapps.content_libraries.api import libraries as lib_api

try:
library_obj = lib_api.ContentLibrary.objects.get_by_key(library_key)
return lib_api.user_has_permission_across_lib_authz_systems(
user, authz_permissions.MANAGE_LIBRARY_TAGS, library_obj
)
except lib_api.ContentLibrary.DoesNotExist:
return False


def check_course_advanced_settings_access(user, course_key, access_type='read'):
"""
Check if user has access to advanced settings for a course.
Expand Down
23 changes: 16 additions & 7 deletions openedx/core/djangoapps/content_libraries/api/libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,15 +336,24 @@ def get_metadata(queryset: QuerySet[ContentLibrary], text_search: str | None = N
return libraries


def require_permission_for_library_key(library_key: LibraryLocatorV2, user: UserType, permission) -> ContentLibrary:
def require_permission_for_library_key(
library_key: LibraryLocatorV2, user: UserType, permission: str | authz_api.data.PermissionData
) -> ContentLibrary:
"""
Given any of the content library permission strings defined in
openedx.core.djangoapps.content_libraries.permissions,
check if the given user has that permission for the library with the
specified library ID.
Check if the user has the specified permission for a content library.

Raises django.core.exceptions.PermissionDenied if the user doesn't have
permission.
Args:
library_key: The library key identifying the content library
user: The user whose permissions are being checked
permission: Either a permission string from content_libraries.permissions
or a PermissionData instance from the authz API

Returns:
ContentLibrary: The library object if permission check passes

Raises:
ContentLibraryNotFound: If the library with the given key doesn't exist
PermissionDenied: If the user doesn't have the required permission
"""
library_obj = ContentLibrary.objects.get_by_key(library_key)
# obj should be able to read any valid model object but mypy thinks it can only be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,9 @@ def post(self, request, lib_key_str):
Import the contents of the user's clipboard and paste them into the Library
"""
library_key = LibraryLocatorV2.from_string(lib_key_str)
api.require_permission_for_library_key(library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
api.require_permission_for_library_key(
library_key, request.user, authz_permissions.REUSE_LIBRARY_CONTENT
)

try:
result = api.import_staged_content_from_user_clipboard(library_key, request.user)
Expand Down
3 changes: 2 additions & 1 deletion openedx/core/djangoapps/content_staging/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locator import CourseLocator, LibraryLocatorV2
from openedx_authz.constants import permissions as authz_permissions
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
from rest_framework.response import Response
from rest_framework.views import APIView
Expand Down Expand Up @@ -114,7 +115,7 @@ def post(self, request):
lib_api.require_permission_for_library_key(
course_key,
request.user,
lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY
authz_permissions.REUSE_LIBRARY_CONTENT
)
block = xblock_api.load_block(usage_key, user=None)
version_num = lib_api.get_library_block(usage_key).draft_version_num
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1945,15 +1945,15 @@ def test_get_copied_tags(self):

@ddt.data(
('staff', 'courseA', 8),
('staff', 'libraryA', 8),
('staff', 'collection_key', 8),
('staff', 'libraryA', 23),
('staff', 'collection_key', 23),
("content_creatorA", 'courseA', 17, False),
("content_creatorA", 'libraryA', 17, False),
("content_creatorA", 'collection_key', 17, False),
("library_staffA", 'libraryA', 17, False), # Library users can only view objecttags, not change them?
("library_staffA", 'collection_key', 17, False),
("library_userA", 'libraryA', 17, False),
("library_userA", 'collection_key', 17, False),
("content_creatorA", 'libraryA', 28, False),
("content_creatorA", 'collection_key', 28, False),
("library_staffA", 'libraryA', 28, False), # Library users can only view objecttags, not change them?
("library_staffA", 'collection_key', 28, False),
("library_userA", 'libraryA', 28, False),
("library_userA", 'collection_key', 28, False),
("instructorA", 'courseA', 17),
("course_instructorA", 'courseA', 17),
("course_staffA", 'courseA', 17),
Expand Down
13 changes: 11 additions & 2 deletions openedx/core/djangoapps/content_tagging/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
import django.contrib.auth.models
import openedx_tagging.rules as oel_tagging
import rules
from opaque_keys.edx.locator import LibraryLocatorV2
from organizations.models import Organization

from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
from common.djangoapps.student.auth import has_library_tagging_access, has_studio_read_access, has_studio_write_access
from common.djangoapps.student.role_helpers import get_course_roles, get_role_cache
from common.djangoapps.student.roles import (
CourseInstructorRole,
Expand Down Expand Up @@ -218,7 +219,10 @@ def can_change_taxonomy(user: UserType, taxonomy: oel_tagging.Taxonomy) -> bool:
@rules.predicate
def can_change_object_tag_objectid(user: UserType, object_id: str) -> bool:
"""
Everyone that has permission to edit the object should be able to tag it.
Check if user has permission to tag the object.

For Content Libraries V2: requires MANAGE_LIBRARY_TAGS permission.
For other contexts (courses, etc.): requires edit/write access.
"""
if not object_id:
return True
Expand All @@ -229,6 +233,11 @@ def can_change_object_tag_objectid(user: UserType, object_id: str) -> bool:
except (ValueError, AssertionError):
return False

# For Content Libraries V2, check specific tagging permission
if isinstance(context_key, LibraryLocatorV2) and has_library_tagging_access(user, context_key):
return True

# For other contexts (courses, xblocks, etc.), use general write access
if has_studio_write_access(user, context_key):
return True

Expand Down
Loading