Skip to content

Commit

Permalink
fix: content libraries permissions
Browse files Browse the repository at this point in the history
This PR changes the permissions for content libraries so that only
people who can create courses should be allowed to create new content
libraries.
  • Loading branch information
rpenido authored and ormsbee committed Dec 6, 2024
1 parent 0dd9f8f commit 8d4909a
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def get(self, request: Request):
"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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def setUp(self):
"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,
Expand Down
8 changes: 8 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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),
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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),
Expand Down
24 changes: 12 additions & 12 deletions cms/djangoapps/contentstore/views/tests/test_course_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,13 +433,13 @@ def check_index_page(self, separate_archived_courses, org):
@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):
Expand All @@ -464,13 +464,13 @@ def test_separate_archived_courses(self, separate_archived_courses, username, or
@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(
Expand Down
7 changes: 7 additions & 0 deletions openedx/core/djangoapps/content_libraries/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 12 additions & 3 deletions openedx/core/djangoapps/content_libraries/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -68,16 +74,19 @@ def is_studio_request(_):

# 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?
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
)
Expand Down
2 changes: 1 addition & 1 deletion openedx/core/djangoapps/content_libraries/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class ContentLibrariesRestApiTest(APITransactionTestCase):

def setUp(self):
super().setUp()
self.user = UserFactory.create(username="Bob", email="[email protected]", password="edx")
self.user = UserFactory.create(username="Bob", email="[email protected]", password="edx", is_staff=True)
# Create an organization
self.organization, _ = Organization.objects.get_or_create(
short_name="CL-TEST",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ def test_library_blocks(self): # pylint: disable=too-many-statements
Tests with some non-ASCII chars in slugs, titles, descriptions.
"""
admin = UserFactory.create(username="Admin", email="[email protected]", 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
Expand Down Expand Up @@ -531,7 +533,7 @@ def test_library_permissions(self): # pylint: disable=too-many-statements
Learning Core data models.
"""
# Create a few users to use for all of these tests:
admin = UserFactory.create(username="Admin", email="[email protected]")
admin = UserFactory.create(username="Admin", email="[email protected]", is_staff=True)
author = UserFactory.create(username="Author", email="[email protected]")
reader = UserFactory.create(username="Reader", email="[email protected]")
group = Group.objects.create(name="group1")
Expand Down Expand Up @@ -653,14 +655,15 @@ def test_library_permissions(self): # pylint: disable=too-many-statements
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")

Expand Down Expand Up @@ -702,7 +705,7 @@ def test_no_lockout(self):
"""
Test that administrators cannot be removed if they are the only administrator granted access.
"""
admin = UserFactory.create(username="Admin", email="[email protected]")
admin = UserFactory.create(username="Admin", email="[email protected]", is_staff=True)
successor = UserFactory.create(username="Successor", email="[email protected]")
with self.as_user(admin):
lib = self._create_library(slug="permtest", title="Permission Test Library", description="Testing")
Expand Down Expand Up @@ -1026,7 +1029,7 @@ def test_library_paste_clipboard(self):
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="[email protected]")
author = UserFactory.create(username="Author", email="[email protected]", is_staff=True)
with self.as_user(author):
lib = self._create_library(
slug="test_lib_paste_clipboard",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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
Expand All @@ -34,7 +35,6 @@
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
Expand Down Expand Up @@ -289,8 +289,8 @@ def setUp(self):
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
Expand Down Expand Up @@ -510,12 +510,12 @@ def test_create_taxonomy(self, user_attr: str, expected_status: int) -> None:

@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):
Expand Down Expand Up @@ -1879,16 +1879,16 @@ def test_get_copied_tags(self):
('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(
Expand Down

0 comments on commit 8d4909a

Please sign in to comment.