Merge pull request #25088 from edx/pwnage101/course-id-list-view-bypass-has-access
Bypass calls to has_access() when listing courses
This commit is contained in:
@@ -18,8 +18,10 @@ from lms.djangoapps.courseware.courses import (
|
|||||||
get_courses,
|
get_courses,
|
||||||
get_permission_for_course_about
|
get_permission_for_course_about
|
||||||
)
|
)
|
||||||
|
from opaque_keys.edx.django.models import CourseKeyField
|
||||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||||
from openedx.core.lib.api.view_utils import LazySequence
|
from openedx.core.lib.api.view_utils import LazySequence
|
||||||
|
from student.models import CourseAccessRole
|
||||||
from student.roles import GlobalStaff
|
from student.roles import GlobalStaff
|
||||||
from xmodule.modulestore.django import modulestore
|
from xmodule.modulestore.django import modulestore
|
||||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||||
@@ -170,19 +172,43 @@ def list_course_keys(request, username, role):
|
|||||||
"""
|
"""
|
||||||
user = get_effective_user(request.user, username)
|
user = get_effective_user(request.user, username)
|
||||||
|
|
||||||
course_keys = CourseOverview.get_all_course_keys()
|
all_course_keys = CourseOverview.get_all_course_keys()
|
||||||
|
|
||||||
# Global staff have access to all courses. Filter courses for non-global staff.
|
# Global staff have access to all courses. Filter courses for non-global staff.
|
||||||
if GlobalStaff().has_user(user):
|
if GlobalStaff().has_user(user):
|
||||||
return course_keys
|
return all_course_keys
|
||||||
|
|
||||||
return LazySequence(
|
if role == 'staff':
|
||||||
(
|
# This short-circuit implementation bypasses has_access() which we think is too slow for some users when
|
||||||
course_key for course_key in course_keys
|
# evaluating staff-level course access for Insights. Various tickets have context on this issue: CR-2487,
|
||||||
if has_access(user, role, course_key)
|
# TNL-7448, DESUPPORT-416, and probably more.
|
||||||
),
|
#
|
||||||
est_len=len(course_keys)
|
# This is a simplified implementation that does not consider org-level access grants (e.g. when course_id is
|
||||||
)
|
# empty).
|
||||||
|
filtered_course_keys = (
|
||||||
|
CourseAccessRole.objects.filter(
|
||||||
|
user=user,
|
||||||
|
# Having the instructor role implies staff access.
|
||||||
|
role__in=['staff', 'instructor'],
|
||||||
|
)
|
||||||
|
# We need to check against CourseOverview so that we don't return any Libraries.
|
||||||
|
.extra(tables=['course_overviews_courseoverview'], where=['course_id = course_overviews_courseoverview.id'])
|
||||||
|
# For good measure, make sure we don't return empty course IDs.
|
||||||
|
.exclude(course_id=CourseKeyField.Empty)
|
||||||
|
.order_by('course_id')
|
||||||
|
.values_list('course_id', flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# This is the original implementation which still covers the case where role = "instructor":
|
||||||
|
filtered_course_keys = LazySequence(
|
||||||
|
(
|
||||||
|
course_key for course_key in all_course_keys
|
||||||
|
if has_access(user, role, course_key)
|
||||||
|
),
|
||||||
|
est_len=len(all_course_keys)
|
||||||
|
)
|
||||||
|
return filtered_course_keys
|
||||||
|
|
||||||
|
|
||||||
def get_due_dates(request, course_key, user):
|
def get_due_dates(request, course_key, user):
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from waffle.testutils import override_switch
|
|||||||
|
|
||||||
from course_modes.models import CourseMode
|
from course_modes.models import CourseMode
|
||||||
from course_modes.tests.factories import CourseModeFactory
|
from course_modes.tests.factories import CourseModeFactory
|
||||||
|
from opaque_keys.edx.locator import LibraryLocator
|
||||||
from openedx.core.lib.api.view_utils import LazySequence
|
from openedx.core.lib.api.view_utils import LazySequence
|
||||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||||
@@ -459,9 +460,15 @@ class CourseIdListViewTestCase(CourseApiTestViewMixin, ModuleStoreTestCase):
|
|||||||
add_users(self.global_admin, CourseStaffRole(self.course.id), course_staff_user)
|
add_users(self.global_admin, CourseStaffRole(self.course.id), course_staff_user)
|
||||||
|
|
||||||
# Create a second course, along with an instructor user for it.
|
# Create a second course, along with an instructor user for it.
|
||||||
alternate_course = self.create_course(org='test')
|
alternate_course1 = self.create_course(org='test1')
|
||||||
course_instructor_user = self.create_user(username='course_instructor', is_staff=False)
|
course_instructor_user = self.create_user(username='course_instructor', is_staff=False)
|
||||||
add_users(self.global_admin, CourseInstructorRole(alternate_course.id), course_instructor_user)
|
add_users(self.global_admin, CourseInstructorRole(alternate_course1.id), course_instructor_user)
|
||||||
|
|
||||||
|
# Create a third course, along with an user that has both staff and instructor for it.
|
||||||
|
alternate_course2 = self.create_course(org='test2')
|
||||||
|
course_instructor_staff_user = self.create_user(username='course_instructor_staff', is_staff=False)
|
||||||
|
add_users(self.global_admin, CourseInstructorRole(alternate_course2.id), course_instructor_staff_user)
|
||||||
|
add_users(self.global_admin, CourseStaffRole(alternate_course2.id), course_instructor_staff_user)
|
||||||
|
|
||||||
# Requesting the courses for which the course staff user is staff should return *only* the single course.
|
# Requesting the courses for which the course staff user is staff should return *only* the single course.
|
||||||
self.setup_user(self.staff_user)
|
self.setup_user(self.staff_user)
|
||||||
@@ -485,7 +492,7 @@ class CourseIdListViewTestCase(CourseApiTestViewMixin, ModuleStoreTestCase):
|
|||||||
'role': 'instructor'
|
'role': 'instructor'
|
||||||
})
|
})
|
||||||
self.assertEqual(len(filtered_response.data['results']), 1)
|
self.assertEqual(len(filtered_response.data['results']), 1)
|
||||||
self.assertTrue(filtered_response.data['results'][0].startswith(alternate_course.org))
|
self.assertTrue(filtered_response.data['results'][0].startswith(alternate_course1.org))
|
||||||
|
|
||||||
# The course instructor user has the inferred course staff role on one course.
|
# The course instructor user has the inferred course staff role on one course.
|
||||||
self.setup_user(course_instructor_user)
|
self.setup_user(course_instructor_user)
|
||||||
@@ -494,7 +501,38 @@ class CourseIdListViewTestCase(CourseApiTestViewMixin, ModuleStoreTestCase):
|
|||||||
'role': 'staff'
|
'role': 'staff'
|
||||||
})
|
})
|
||||||
self.assertEqual(len(filtered_response.data['results']), 1)
|
self.assertEqual(len(filtered_response.data['results']), 1)
|
||||||
self.assertTrue(filtered_response.data['results'][0].startswith(alternate_course.org))
|
self.assertTrue(filtered_response.data['results'][0].startswith(alternate_course1.org))
|
||||||
|
|
||||||
|
# The user with both instructor AND staff on a course has the inferred course staff role on that one course.
|
||||||
|
self.setup_user(course_instructor_staff_user)
|
||||||
|
filtered_response = self.verify_response(params={
|
||||||
|
'username': course_instructor_staff_user.username,
|
||||||
|
'role': 'staff'
|
||||||
|
})
|
||||||
|
self.assertEqual(len(filtered_response.data['results']), 1)
|
||||||
|
self.assertTrue(filtered_response.data['results'][0].startswith(alternate_course2.org))
|
||||||
|
|
||||||
|
def test_no_libraries(self):
|
||||||
|
"""
|
||||||
|
Verify that only Course IDs are returned, not anything else like libraries.
|
||||||
|
"""
|
||||||
|
# Make this user a course staff user for a course, AND a library.
|
||||||
|
course_staff_user = self.create_user(username='course_staff', is_staff=False)
|
||||||
|
add_users(self.global_admin, CourseStaffRole(self.course.id), course_staff_user)
|
||||||
|
add_users(
|
||||||
|
self.global_admin,
|
||||||
|
CourseStaffRole(LibraryLocator.from_string('library-v1:library_org+library_name')),
|
||||||
|
course_staff_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Requesting the courses should return *only* courses and not libraries.
|
||||||
|
self.setup_user(self.staff_user)
|
||||||
|
filtered_response = self.verify_response(params={
|
||||||
|
'username': course_staff_user.username,
|
||||||
|
'role': 'staff'
|
||||||
|
})
|
||||||
|
self.assertEqual(len(filtered_response.data['results']), 1)
|
||||||
|
self.assertTrue(filtered_response.data['results'][0].startswith(self.course.org))
|
||||||
|
|
||||||
|
|
||||||
class LazyPageNumberPaginationTestCase(TestCase):
|
class LazyPageNumberPaginationTestCase(TestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user