Skip to content
Closed
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
9 changes: 9 additions & 0 deletions lms/djangoapps/course_home_api/outline/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,16 @@ def completions_dict(self):

Dictionary keys are block keys and values are int values
representing the completion status of the block.

Anonymous users (who can reach this view on public courses via the
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG waffle) cannot own BlockCompletion
rows, so short-circuit with an empty mapping. Querying BlockCompletion
with an AnonymousUser otherwise raises TypeError because the FK
coercion cannot cast AnonymousUser to an integer primary key.
See bug #38019.
"""
if self.request.user.is_anonymous:
return {}
course_key_string = self.kwargs.get('course_key_string')
course_key = CourseKey.from_string(course_key_string)
completions = BlockCompletion.objects.filter(user=self.request.user, context_key=course_key).values_list(
Expand Down
21 changes: 21 additions & 0 deletions lms/djangoapps/courseware/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
ACCESS_DENIED,
ACCESS_GRANTED,
check_course_open_for_learner,
check_public_access,
check_start_date,
debug,
)
Expand All @@ -67,6 +68,7 @@
from xmodule.course_block import ( # lint-amnesty, pylint: disable=wrong-import-order
CATALOG_VISIBILITY_ABOUT,
CATALOG_VISIBILITY_CATALOG_AND_ABOUT,
COURSE_VISIBILITY_PUBLIC,
CourseBlock,
)
from xmodule.error_block import ErrorBlock # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -366,6 +368,14 @@ def can_load():
if courselike.id.deprecated: # we no longer support accessing Old Mongo courses
return OldMongoAccessError(courselike)

# Anonymous users on a public course (with the unenrolled-access waffle
# enabled) must be allowed to load the course even when the course has
# a future start date. Without this, check_course_open_for_learner
# below returns StartDateError and render_xblock converts it to 404,
# breaking anonymous video viewing on public courses. See bug #38019.
if user.is_anonymous and check_public_access(courselike, [COURSE_VISIBILITY_PUBLIC]):
return ACCESS_GRANTED

visible_to_nonstaff = _visible_to_nonstaff_users(courselike)
if not visible_to_nonstaff:
staff_access = _has_staff_access_to_block(user, courselike, courselike.id)
Expand Down Expand Up @@ -609,6 +619,17 @@ def can_load():
if not group_access_response:
return group_access_response

# Anonymous users on a public course must be able to load blocks even
# when the block's (inherited) start date is in the future. The course
# level check is already handled in _has_access_course; mirror that
# bypass here so block-level checks agree (bug #38019). We still honor
# group access (checked above) so cohort/partition gating continues
# to apply.
if user.is_anonymous and course_key is not None:
course_overview = CourseOverview.get_from_id(course_key)
if check_public_access(course_overview, [COURSE_VISIBILITY_PUBLIC]):
return ACCESS_GRANTED

# If the user has staff access, they can load the block and checks below are not needed.
staff_access_response = _has_staff_access_to_block(user, block, course_key)
if staff_access_response:
Expand Down
76 changes: 75 additions & 1 deletion lms/djangoapps/courseware/tests/test_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.course_experience import ENFORCE_MASQUERADE_START_DATES
from openedx.features.course_experience import (
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
ENFORCE_MASQUERADE_START_DATES,
)
from openedx.features.enterprise_support.api import add_enterprise_customer_to_session
from openedx.features.enterprise_support.tests.factories import (
EnterpriseCourseEnrollmentFactory,
Expand Down Expand Up @@ -729,6 +732,77 @@ def test__catalog_visibility_returns_typed_error(self):
assert isinstance(see_in_catalog_response, access_response.CatalogVisibilityError)
assert access._has_access_course(user, 'see_about_page', course_about)

@override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=True)
def test_unit_anonymous_can_load_public_course_with_future_start(self):
"""
Unit regression for bug #38019: anonymous users on public courses
with a future start date must be granted load access. Previously
denied by check_course_open_for_learner -> StartDateError.
"""
from xmodule.course_block import COURSE_VISIBILITY_PUBLIC
future_course = CourseFactory.create(
org='edX', course='public38019', run='future',
start=self.DATES[self.TOMORROW],
course_visibility=COURSE_VISIBILITY_PUBLIC,
)
response = access._has_access_course(self.anonymous_user, 'load', future_course)
assert bool(response) is True

@override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=False)
def test_unit_anonymous_denied_when_unenrolled_access_flag_off(self):
"""
Guard rail: disabling the unenrolled-access waffle must still deny
anonymous users even on a public course. Bug #38019.
"""
from xmodule.course_block import COURSE_VISIBILITY_PUBLIC
future_course = CourseFactory.create(
org='edX', course='public38019', run='flagoff',
start=self.DATES[self.TOMORROW],
course_visibility=COURSE_VISIBILITY_PUBLIC,
)
response = access._has_access_course(self.anonymous_user, 'load', future_course)
assert bool(response) is False

@override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=True)
def test_integration_anonymous_can_load_block_on_public_course(self):
"""
Integration regression for bug #38019: anonymous block-level load
access must succeed on public courses even when the block inherits
a future start date.
"""
from xmodule.course_block import COURSE_VISIBILITY_PUBLIC
future_course = CourseFactory.create(
org='edX', course='public38019', run='blocktest',
start=self.DATES[self.TOMORROW],
course_visibility=COURSE_VISIBILITY_PUBLIC,
)
mock_block = Mock(
user_partitions=[],
group_access={},
start=self.DATES[self.TOMORROW],
days_early_for_beta=None,
merged_group_access={},
visible_to_staff_only=False,
location=future_course.id.make_usage_key('video', 'sample'),
)
response = access._has_access_to_block(
self.anonymous_user, 'load', mock_block, course_key=future_course.id,
)
assert bool(response) is True

def test_bug_38019_regression_anonymous_still_denied_on_non_public_future(self):
"""
Regression for bug #38019: the public-access bypass must not leak
to non-public courses. Anonymous users must still be denied on
non-public courses with future start dates.
"""
future_course = CourseFactory.create(
org='edX', course='private38019', run='future',
start=self.DATES[self.TOMORROW],
)
response = access._has_access_course(self.anonymous_user, 'load', future_course)
assert bool(response) is False

@patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True})
@override_settings(MILESTONES_APP=True)
def test_access_on_course_with_pre_requisites(self):
Expand Down