Skip to content

Commit 5d1566c

Browse files
authored
Merge pull request #35655 from mitodl/asad/bypass-access-checks-when-populating-cache
fix: bypass access checks when populating course blocks cache
2 parents 767e1a2 + 361ed61 commit 5d1566c

File tree

3 files changed

+169
-4
lines changed

3 files changed

+169
-4
lines changed

lms/djangoapps/course_blocks/api.py

+1
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,5 @@ def get_course_blocks(
108108
transformers,
109109
starting_block_usage_key,
110110
collected_block_structure,
111+
user,
111112
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# pylint: disable=attribute-defined-outside-init
2+
"""
3+
Tests for course_blocks API
4+
"""
5+
6+
from unittest.mock import Mock, patch
7+
8+
import ddt
9+
from django.http.request import HttpRequest
10+
11+
from common.djangoapps.student.tests.factories import UserFactory
12+
from lms.djangoapps.course_blocks.api import get_course_blocks
13+
from lms.djangoapps.course_blocks.transformers.tests.helpers import CourseStructureTestCase
14+
from lms.djangoapps.course_blocks.transformers.tests.test_user_partitions import UserPartitionTestMixin
15+
from lms.djangoapps.courseware.block_render import make_track_function, prepare_runtime_for_user
16+
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
17+
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort
18+
from xmodule.modulestore.django import modulestore
19+
20+
21+
def get_block_side_effect(block_locator, user_known):
22+
"""
23+
Side effect for `CachingDescriptorSystem.get_block`
24+
"""
25+
store = modulestore()
26+
course = store.get_course(block_locator.course_key)
27+
block = store.get_item(block_locator)
28+
runtime = block.runtime
29+
user = UserFactory.create()
30+
user.known = user_known
31+
32+
prepare_runtime_for_user(
33+
user=user,
34+
student_data=Mock(),
35+
runtime=runtime,
36+
course_id=block_locator.course_key,
37+
track_function=make_track_function(HttpRequest()),
38+
request_token=Mock(),
39+
course=course,
40+
)
41+
return block.runtime.get_block_for_descriptor(block)
42+
43+
44+
def get_block_side_effect_for_known_user(self, *args, **kwargs):
45+
"""
46+
Side effect for known user test.
47+
"""
48+
return get_block_side_effect(self, True)
49+
50+
51+
def get_block_side_effect_for_unknown_user(self, *args, **kwargs):
52+
"""
53+
Side effect for unknown user test.
54+
"""
55+
return get_block_side_effect(self, False)
56+
57+
58+
@ddt.ddt
59+
class TestGetCourseBlocks(UserPartitionTestMixin, CourseStructureTestCase):
60+
"""
61+
Tests `get_course_blocks` API
62+
"""
63+
64+
def setup_partitions_and_course(self):
65+
"""
66+
Setup course structure.
67+
"""
68+
# Set up user partitions and groups.
69+
self.setup_groups_partitions(active=True, num_groups=1)
70+
self.user_partition = self.user_partitions[0]
71+
72+
# Build course.
73+
self.course_hierarchy = self.get_course_hierarchy()
74+
self.blocks = self.build_course(self.course_hierarchy)
75+
self.course = self.blocks['course']
76+
77+
# Set up cohorts.
78+
self.setup_cohorts(self.course)
79+
80+
def get_course_hierarchy(self):
81+
"""
82+
Returns a course hierarchy to test with.
83+
"""
84+
# course
85+
# / \
86+
# / \
87+
# A[0] B
88+
# |
89+
# |
90+
# O
91+
92+
return [
93+
{
94+
'org': 'UserPartitionTransformer',
95+
'course': 'UP101F',
96+
'run': 'test_run',
97+
'user_partitions': [self.user_partition],
98+
'#type': 'course',
99+
'#ref': 'course',
100+
'#children': [
101+
{
102+
'#type': 'vertical',
103+
'#ref': 'A',
104+
'metadata': {'group_access': {self.user_partition.id: [0]}},
105+
},
106+
{'#type': 'vertical', '#ref': 'B'},
107+
],
108+
},
109+
{
110+
'#type': 'vertical',
111+
'#ref': 'O',
112+
'#parents': ['B'],
113+
},
114+
]
115+
116+
@ddt.data(
117+
(1, ('course', 'B', 'O'), True),
118+
(1, ('course', 'A', 'B', 'O'), False),
119+
(None, ('course', 'B', 'O'), True),
120+
(None, ('course', 'A', 'B', 'O'), False),
121+
)
122+
@ddt.unpack
123+
def test_get_course_blocks(self, group_id, expected_blocks, user_known):
124+
"""
125+
Tests that `get_course_blocks` returns blocks without access checks for unknown users.
126+
127+
Access checks are done through the transformers and through Runtime get_block_for_descriptor. Due
128+
to the runtime limitations during the tests, the Runtime access checks are not performed as
129+
get_block_for_descriptor is never called and Block is returned by CachingDescriptorSystem.get_block.
130+
In this test, we mock the CachingDescriptorSystem.get_block and check block access for known and unknown users.
131+
For known users, it performs the Runtime access checks through get_block_for_descriptor. For unknown, it
132+
skips the access checks.
133+
"""
134+
self.setup_partitions_and_course()
135+
if group_id:
136+
cohort = self.partition_cohorts[self.user_partition.id - 1][group_id - 1]
137+
add_user_to_cohort(cohort, self.user.username)
138+
139+
side_effect = get_block_side_effect_for_known_user if user_known else get_block_side_effect_for_unknown_user
140+
with patch('xmodule.modulestore.split_mongo.split.CachingDescriptorSystem.get_block', side_effect=side_effect):
141+
block_structure = get_course_blocks(
142+
self.user,
143+
self.course.location,
144+
BlockStructureTransformers([]),
145+
)
146+
self.assertSetEqual(
147+
set(block_structure.get_block_keys()),
148+
self.get_block_key_set(self.blocks, *expected_blocks)
149+
)

openedx/core/djangoapps/content/block_structure/manager.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __init__(self, root_block_usage_key, modulestore, cache):
3535
self.modulestore = modulestore
3636
self.store = BlockStructureStore(cache)
3737

38-
def get_transformed(self, transformers, starting_block_usage_key=None, collected_block_structure=None):
38+
def get_transformed(self, transformers, starting_block_usage_key=None, collected_block_structure=None, user=None):
3939
"""
4040
Returns the transformed Block Structure for the root_block_usage_key,
4141
starting at starting_block_usage_key, getting block data from the cache
@@ -57,11 +57,14 @@ def get_transformed(self, transformers, starting_block_usage_key=None, collected
5757
get_collected. Can be optionally provided if already available,
5858
for optimization.
5959
60+
user (django.contrib.auth.models.User) - User object for
61+
which the block structure is to be transformed.
62+
6063
Returns:
6164
BlockStructureBlockData - A transformed block structure,
6265
starting at starting_block_usage_key.
6366
"""
64-
block_structure = collected_block_structure.copy() if collected_block_structure else self.get_collected()
67+
block_structure = collected_block_structure.copy() if collected_block_structure else self.get_collected(user)
6568

6669
if starting_block_usage_key:
6770
# Override the root_block_usage_key so traversals start at the
@@ -77,7 +80,7 @@ def get_transformed(self, transformers, starting_block_usage_key=None, collected
7780
transformers.transform(block_structure)
7881
return block_structure
7982

80-
def get_collected(self):
83+
def get_collected(self, user=None):
8184
"""
8285
Returns the collected Block Structure for the root_block_usage_key,
8386
getting block data from the cache and modulestore, as needed.
@@ -86,6 +89,10 @@ def get_collected(self):
8689
the modulestore is accessed if needed (at cache miss), and the
8790
transformers data is collected if needed.
8891
92+
In the case of a cache miss, the function bypasses runtime access checks for the current
93+
user. This is done to prevent inconsistencies in the data, which can occur when
94+
certain blocks are inaccessible due to access restrictions.
95+
8996
Returns:
9097
BlockStructureBlockData - A collected block structure,
9198
starting at root_block_usage_key, with collected data
@@ -99,7 +106,15 @@ def get_collected(self):
99106
BlockStructureTransformers.verify_versions(block_structure)
100107

101108
except (BlockStructureNotFound, TransformerDataIncompatible):
102-
block_structure = self._update_collected()
109+
if user and getattr(user, "known", True):
110+
# This bypasses the runtime access checks. When we are populating the course blocks cache,
111+
# we do not want to perform access checks. Access checks result in inconsistent blocks where
112+
# inaccessible blocks are missing from the cache. Cached course blocks are then used for all the users.
113+
user.known = False
114+
block_structure = self._update_collected()
115+
user.known = True
116+
else:
117+
block_structure = self._update_collected()
103118

104119
return block_structure
105120

0 commit comments

Comments
 (0)