fix: content libraries permissions

This PR changes the permissions for content libraries so that only
people who can create courses should be allowed to create new content
libraries.
This commit is contained in:
Rômulo Penido
2024-12-05 17:37:10 -03:00
committed by David Ormsbee
parent 0dd9f8f996
commit 8d4909a999
10 changed files with 71 additions and 41 deletions

View File

@@ -66,6 +66,7 @@ class StudioHomeSerializer(serializers.Serializer):
request_course_creator_url = serializers.CharField()
rerun_creator_status = serializers.BooleanField()
show_new_library_button = serializers.BooleanField()
show_new_library_v2_button = serializers.BooleanField()
split_studio_home = serializers.BooleanField()
studio_name = serializers.CharField()
studio_short_name = serializers.CharField()

View File

@@ -66,6 +66,7 @@ class HomePageView(APIView):
"request_course_creator_url": "/request_course_creator",
"rerun_creator_status": true,
"show_new_library_button": true,
"show_new_library_v2_button": true,
"split_studio_home": false,
"studio_name": "Studio",
"studio_short_name": "Studio",

View File

@@ -56,6 +56,7 @@ class HomePageViewTest(CourseTestCase):
"request_course_creator_url": "/request_course_creator",
"rerun_creator_status": True,
"show_new_library_button": True,
"show_new_library_v2_button": True,
"split_studio_home": False,
"studio_name": settings.STUDIO_NAME,
"studio_short_name": settings.STUDIO_SHORT_NAME,

View File

@@ -1552,6 +1552,9 @@ def get_library_context(request, request_is_json=False):
from cms.djangoapps.contentstore.views.library import (
user_can_view_create_library_button,
)
from openedx.core.djangoapps.content_libraries.api import (
user_can_create_library,
)
libraries = _accessible_libraries_iter(request.user) if libraries_v1_enabled() else []
data = {
@@ -1565,6 +1568,7 @@ def get_library_context(request, request_is_json=False):
'courses': [],
'libraries_enabled': libraries_v1_enabled(),
'show_new_library_button': user_can_view_create_library_button(request.user) and request.user.is_active,
'show_new_library_v2_button': user_can_create_library(request.user),
'user': request.user,
'request_course_creator_url': reverse('request_course_creator'),
'course_creator_status': _get_course_creator_status(request.user),
@@ -1686,6 +1690,9 @@ def get_home_context(request, no_course=False):
from cms.djangoapps.contentstore.views.library import (
user_can_view_create_library_button,
)
from openedx.core.djangoapps.content_libraries.api import (
user_can_create_library,
)
active_courses = []
archived_courses = []
@@ -1714,6 +1721,7 @@ def get_home_context(request, no_course=False):
'taxonomy_list_mfe_url': get_taxonomy_list_url(),
'libraries': libraries,
'show_new_library_button': user_can_view_create_library_button(user),
'show_new_library_v2_button': user_can_create_library(user),
'user': user,
'request_course_creator_url': reverse('request_course_creator'),
'course_creator_status': _get_course_creator_status(user),

View File

@@ -433,13 +433,13 @@ class TestCourseIndexArchived(CourseTestCase):
@override_settings(FEATURES=FEATURES_WITHOUT_HOME_PAGE_COURSE_V2_API)
@ddt.data(
# Staff user has course staff access
(True, 'staff', None, 0, 21),
(False, 'staff', None, 0, 21),
(True, 'staff', None, 0, 23),
(False, 'staff', None, 0, 23),
# Base user has global staff access
(True, 'user', ORG, 2, 21),
(False, 'user', ORG, 2, 21),
(True, 'user', None, 2, 21),
(False, 'user', None, 2, 21),
(True, 'user', ORG, 2, 23),
(False, 'user', ORG, 2, 23),
(True, 'user', None, 2, 23),
(False, 'user', None, 2, 23),
)
@ddt.unpack
def test_separate_archived_courses(self, separate_archived_courses, username, org, mongo_queries, sql_queries):
@@ -464,13 +464,13 @@ class TestCourseIndexArchived(CourseTestCase):
@override_settings(FEATURES=FEATURES_WITH_HOME_PAGE_COURSE_V2_API)
@ddt.data(
# Staff user has course staff access
(True, 'staff', None, 0, 21),
(False, 'staff', None, 0, 21),
(True, 'staff', None, 0, 23),
(False, 'staff', None, 0, 23),
# Base user has global staff access
(True, 'user', ORG, 0, 21),
(False, 'user', ORG, 0, 21),
(True, 'user', None, 0, 21),
(False, 'user', None, 0, 21),
(True, 'user', ORG, 0, 23),
(False, 'user', ORG, 0, 23),
(True, 'user', None, 0, 23),
(False, 'user', None, 0, 23),
)
@ddt.unpack
def test_separate_archived_courses_with_home_page_course_v2_api(

View File

@@ -308,6 +308,13 @@ class LibraryXBlockType:
# ============
def user_can_create_library(user: AbstractUser) -> bool:
"""
Check if the user has permission to create a content library.
"""
return user.has_perm(permissions.CAN_CREATE_CONTENT_LIBRARY)
def get_libraries_for_user(user, org=None, text_search=None, order=None):
"""
Return content libraries that the user has permission to view.

View File

@@ -48,6 +48,12 @@ def is_studio_request(_):
return settings.SERVICE_VARIANT == "cms"
@blanket_rule
def is_course_creator(user):
from cms.djangoapps.course_creators.views import get_course_creator_status
return get_course_creator_status(user) == 'granted'
########################### Permissions ###########################
# Is the user allowed to view XBlocks from the specified content library
@@ -68,7 +74,10 @@ perms[CAN_LEARN_FROM_THIS_CONTENT_LIBRARY] = (
# Is the user allowed to create content libraries?
CAN_CREATE_CONTENT_LIBRARY = 'content_libraries.create_library'
perms[CAN_CREATE_CONTENT_LIBRARY] = is_user_active
if settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
perms[CAN_CREATE_CONTENT_LIBRARY] = is_global_staff | (is_user_active & is_course_creator)
else:
perms[CAN_CREATE_CONTENT_LIBRARY] = is_global_staff
# Is the user allowed to view the specified content library in Studio,
# including to view the raw OLX and asset files?
@@ -76,8 +85,8 @@ CAN_VIEW_THIS_CONTENT_LIBRARY = 'content_libraries.view_library'
perms[CAN_VIEW_THIS_CONTENT_LIBRARY] = is_user_active & (
# Global staff can access any library
is_global_staff |
# Some libraries allow anyone to view them in Studio:
Attribute('allow_public_read', True) |
# Libraries with "public read" permissions can be accessed only by course creators
(Attribute('allow_public_read', True) & is_course_creator) |
# Otherwise the user must be part of the library's team
has_explicit_read_permission_for_library
)

View File

@@ -71,7 +71,7 @@ class ContentLibrariesRestApiTest(APITransactionTestCase):
def setUp(self):
super().setUp()
self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx", is_staff=True)
# Create an organization
self.organization, _ = Organization.objects.get_or_create(
short_name="CL-TEST",

View File

@@ -259,6 +259,8 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
Tests with some non-ASCII chars in slugs, titles, descriptions.
"""
admin = UserFactory.create(username="Admin", email="admin@example.com", is_staff=True)
lib = self._create_library(slug="téstlꜟط", title="A Tést Lꜟطrary", description="Tésting XBlocks")
lib_id = lib["id"]
assert lib['has_unpublished_changes'] is False
@@ -531,7 +533,7 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
Learning Core data models.
"""
# Create a few users to use for all of these tests:
admin = UserFactory.create(username="Admin", email="admin@example.com")
admin = UserFactory.create(username="Admin", email="admin@example.com", is_staff=True)
author = UserFactory.create(username="Author", email="author@example.com")
reader = UserFactory.create(username="Reader", email="reader@example.com")
group = Group.objects.create(name="group1")
@@ -653,14 +655,15 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
self._get_library_block_asset(block3_key, file_name="static/whatever.png", expect_response=403)
# Nor can they preview the block:
self._render_block_view(block3_key, view_name="student_view", expect_response=403)
# But if we grant allow_public_read, then they can:
# Even if we grant allow_public_read, then they can't:
with self.as_user(admin):
self._update_library(lib_id, allow_public_read=True)
self._set_library_block_asset(block3_key, "static/whatever.png", b"data")
with self.as_user(random_user):
self._get_library_block_olx(block3_key)
self._get_library_block_olx(block3_key, expect_response=403)
self._get_library_block_fields(block3_key, expect_response=403)
# But he can preview the block:
self._render_block_view(block3_key, view_name="student_view")
f = self._get_library_block_fields(block3_key)
# self._get_library_block_assets(block3_key)
# self._get_library_block_asset(block3_key, file_name="whatever.png")
@@ -702,7 +705,7 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
"""
Test that administrators cannot be removed if they are the only administrator granted access.
"""
admin = UserFactory.create(username="Admin", email="admin@example.com")
admin = UserFactory.create(username="Admin", email="admin@example.com", is_staff=True)
successor = UserFactory.create(username="Successor", email="successor@example.com")
with self.as_user(admin):
lib = self._create_library(slug="permtest", title="Permission Test Library", description="Testing")
@@ -1026,7 +1029,7 @@ class ContentLibrariesTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMix
from openedx.core.djangoapps.content_staging.api import save_xblock_to_user_clipboard
# Create user to perform tests on
author = UserFactory.create(username="Author", email="author@example.com")
author = UserFactory.create(username="Author", email="author@example.com", is_staff=True)
with self.as_user(author):
lib = self._create_library(
slug="test_lib_paste_clipboard",

View File

@@ -13,6 +13,7 @@ from urllib.parse import parse_qs, urlparse
import ddt
from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from edx_django_utils.cache import RequestCache
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, LibraryCollectionLocator
from openedx_tagging.core.tagging.models import Tag, Taxonomy
from openedx_tagging.core.tagging.models.system_defined import SystemDefinedTaxonomy
@@ -34,7 +35,6 @@ from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.content_libraries.api import AccessLevel, create_library, set_library_user_permissions
from openedx.core.djangoapps.content_tagging import api as tagging_api
from openedx.core.djangoapps.content_tagging.models import TaxonomyOrg
from openedx.core.djangoapps.content_tagging.utils import rules_cache
from openedx.core.djangolib.testing.utils import skip_unless_cms
from ....tests.test_objecttag_export_helpers import TaggedCourseMixin
@@ -289,8 +289,8 @@ class TestTaxonomyObjectsMixin:
self._setUp_taxonomies()
self._setUp_collection()
# Clear the rules cache in between test runs to keep query counts consistent.
rules_cache.clear()
# Clear all request caches in between test runs to keep query counts consistent.
RequestCache.clear_all_namespaces()
@skip_unless_cms
@@ -510,12 +510,12 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase):
@ddt.data(
('staff', 11),
("content_creatorA", 16),
("library_staffA", 16),
("library_userA", 16),
("instructorA", 16),
("course_instructorA", 16),
("course_staffA", 16),
("content_creatorA", 17),
("library_staffA", 17),
("library_userA", 17),
("instructorA", 17),
("course_instructorA", 17),
("course_staffA", 17),
)
@ddt.unpack
def test_list_taxonomy_query_count(self, user_attr: str, expected_queries: int):
@@ -1879,16 +1879,16 @@ class TestObjectTagViewSet(TestObjectTagMixin, APITestCase):
('staff', 'courseA', 8),
('staff', 'libraryA', 8),
('staff', 'collection_key', 8),
("content_creatorA", 'courseA', 11, False),
("content_creatorA", 'libraryA', 11, False),
("content_creatorA", 'collection_key', 11, False),
("library_staffA", 'libraryA', 11, False), # Library users can only view objecttags, not change them?
("library_staffA", 'collection_key', 11, False),
("library_userA", 'libraryA', 11, False),
("library_userA", 'collection_key', 11, False),
("instructorA", 'courseA', 11),
("course_instructorA", 'courseA', 11),
("course_staffA", 'courseA', 11),
("content_creatorA", 'courseA', 12, False),
("content_creatorA", 'libraryA', 12, False),
("content_creatorA", 'collection_key', 12, False),
("library_staffA", 'libraryA', 12, False), # Library users can only view objecttags, not change them?
("library_staffA", 'collection_key', 12, False),
("library_userA", 'libraryA', 12, False),
("library_userA", 'collection_key', 12, False),
("instructorA", 'courseA', 12),
("course_instructorA", 'courseA', 12),
("course_staffA", 'courseA', 12),
)
@ddt.unpack
def test_object_tags_query_count(