Skip to content
Merged
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
11 changes: 6 additions & 5 deletions lms/djangoapps/course_wiki/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
from django.http import Http404
from django.shortcuts import redirect
from django.utils.deprecation import MiddlewareMixin
from openedx_filters.learning.filters import CoursewareViewStarted
from wiki.models import reverse

from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.courses import get_course_overview_with_access, get_course_with_access
from openedx.core.lib.request_utils import course_id_from_url
from openedx.features.enterprise_support.api import get_enterprise_consent_url
from common.djangoapps.student.models import CourseEnrollment

from xmodule.modulestore.django import modulestore
Expand Down Expand Up @@ -96,10 +96,11 @@ def process_view(self, request, view_func, view_args, view_kwargs): # lint-amne
# we'll redirect them to the course about page
return redirect('about_course', str(course_id))

# If we need enterprise data sharing consent for this course, then redirect to the form.
consent_url = get_enterprise_consent_url(request, str(course_id), source='WikiAccessMiddleware')
if consent_url:
return redirect(consent_url)
# If a plugin requires a redirect for this course, redirect now.
try:
CoursewareViewStarted.run_filter(course_key=course_id, view_name='WikiAccessMiddleware')
except CoursewareViewStarted.RedirectToUrl as exc:
return redirect(exc.redirect_to)

# set the course onto here so that the wiki template can show the course navigation
request.course = course
Expand Down
34 changes: 20 additions & 14 deletions lms/djangoapps/course_wiki/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

from unittest.mock import patch
from django.urls import reverse
from openedx_filters.learning.filters import CoursewareViewStarted

from lms.djangoapps.courseware.tests.tests import LoginEnrollmentTestCase
from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order


class WikiRedirectTestCase(EnterpriseTestConsentRequired, LoginEnrollmentTestCase, ModuleStoreTestCase):
class WikiRedirectTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
"""
Tests for wiki course redirection.
"""
Expand Down Expand Up @@ -202,27 +202,33 @@ def test_create_wiki_with_long_course_id(self):
assert resp.status_code == 200

@patch.dict("django.conf.settings.FEATURES", {'ALLOW_WIKI_ROOT_ACCESS': True})
@patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
def test_consent_required(self, mock_enterprise_customer_for_request):
@patch('openedx_filters.learning.filters.CoursewareViewStarted.run_filter')
def test_filter_redirect(self, mock_run_filter):
"""
Test that enterprise data sharing consent is required when enabled for the various courseware views.
Test that wiki views redirect when the CoursewareViewStarted filter provides a URL.
"""
# ENT-924: Temporary solution to replace sensitive SSO usernames.
mock_enterprise_customer_for_request.return_value = None
redirect_url = 'http://example.com/redirect'
mock_run_filter.side_effect = CoursewareViewStarted.RedirectToUrl(message="redirect", redirect_to=redirect_url)

# Public wikis can be accessed by non-enrolled users, and so direct access is not gated by the consent page
# Public wikis can be accessed by non-enrolled users, and so direct access is not gated by the redirect
course = CourseFactory.create()
course.allow_public_wiki_access = False
course.save()

# However, for private wikis, enrolled users must pass through the consent gate
# However, for private wikis, enrolled users must pass through the filter redirect gate
# (Unenrolled users are redirected to course/about)
course_id = str(course.id)
self.login(self.student, self.password)
self.enroll(course)

for (url, status_code) in (
(reverse('course_wiki', kwargs={'course_id': course_id}), 302),
(f'/courses/{course_id}/wiki/', 200),
):
self.verify_consent_required(self.client, url, status_code=status_code) # lint-amnesty, pylint: disable=no-value-for-parameter
# The course_wiki view is decorated with courseware_view_hooks which calls the filter
url = reverse('course_wiki', kwargs={'course_id': course_id})
response = self.client.get(url)
assert response.status_code == 302
assert response['Location'] == redirect_url

# The wiki middleware (/courses/.../wiki/) also calls the filter
url = f'/courses/{course_id}/wiki/'
response = self.client.get(url)
assert response.status_code == 302
assert response['Location'] == redirect_url
4 changes: 2 additions & 2 deletions lms/djangoapps/course_wiki/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
from wiki.models import Article, URLPath

from lms.djangoapps.course_wiki.utils import course_wiki_slug
from lms.djangoapps.courseware.decorators import courseware_view_hooks
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangolib.markup import Text
from openedx.core.lib.courses import get_course_by_id
from openedx.features.enterprise_support.api import data_sharing_consent_required

log = logging.getLogger(__name__)

Expand All @@ -31,7 +31,7 @@ def root_create(request):
return redirect('wiki:get', path=root.path)


@data_sharing_consent_required
@courseware_view_hooks
def course_wiki_redirect(request, course_id, wiki_path=""):
"""
This redirects to whatever page on the wiki that the course designates
Expand Down
46 changes: 46 additions & 0 deletions lms/djangoapps/courseware/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
Decorators for courseware views.
"""
import functools

from django.shortcuts import redirect
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx_filters.learning.filters import CoursewareViewStarted


def courseware_view_hooks(view_func):
"""
Decorator that calls the CoursewareViewStarted filter before rendering a courseware view.

If any pipeline step raises ``CoursewareViewStarted.RedirectToUrl``, the user is
redirected to that URL. Otherwise, the original view is rendered normally.

Usage::

@courseware_view_hooks
def my_view(request, course_id, ...):
...

Works with both function-based views and ``method_decorator``-wrapped class-based views.
The wrapped view must accept ``course_id`` as its first argument after ``request``, which
binds whether callers pass it positionally or as a URL keyword argument.
"""
@functools.wraps(view_func)
def _wrapper(request, course_id, *args, **kwargs):
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
# Skip bad request which contains a malformed course_id; let the view logic raise an error.
return view_func(request, course_id, *args, **kwargs)

try:
view_name = getattr(view_func, '__name__', '')
CoursewareViewStarted.run_filter(course_key=course_key, view_name=view_name)
Comment thread
pwnage101 marked this conversation as resolved.
Comment thread
pwnage101 marked this conversation as resolved.
except CoursewareViewStarted.RedirectToUrl as exc:
# One of the pipeline steps wants us to block view execution and redirect to a specific URL.
return redirect(exc.redirect_to)

return view_func(request, course_id, *args, **kwargs)

return _wrapper
3 changes: 1 addition & 2 deletions lms/djangoapps/courseware/tests/test_view_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@
from common.djangoapps.student.tests.factories import StaffFactory
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.tests.helpers import CourseAccessTestMixin, LoginEnrollmentTestCase
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory


class TestViewAuth(EnterpriseTestConsentRequired, ModuleStoreTestCase, LoginEnrollmentTestCase):
class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Check that view authentication works properly.
"""
Expand Down
43 changes: 14 additions & 29 deletions lms/djangoapps/courseware/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
from freezegun import freeze_time
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx_filters.learning.filters import CoursewareViewStarted
from pytz import UTC
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
from rest_framework import status
Expand Down Expand Up @@ -111,7 +112,6 @@
EnterpriseCustomerUserFactory,
EnterpriseCustomerFactory
)
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
from openedx.features.enterprise_support.api import add_enterprise_customer_to_session
from enterprise.api.v1.serializers import EnterpriseCustomerSerializer

Expand Down Expand Up @@ -1607,6 +1607,19 @@ def mock_certificate_downloadable_status(
'earned_but_not_available': earned_but_not_available,
}

@patch('openedx_filters.learning.filters.CoursewareViewStarted.run_filter')
def test_redirects_when_courseware_view_filter_raises(self, mock_run_filter):
"""
Redirects to the URL raised by the CoursewareViewStarted filter on progress page URLs.
"""
redirect_url = 'http://example.com/redirect'
mock_run_filter.side_effect = CoursewareViewStarted.RedirectToUrl(message="redirect", redirect_to=redirect_url)

resp = self._get_progress_page(expected_status_code=302)
assert resp['Location'] == redirect_url
resp = self._get_student_progress_page(expected_status_code=302)
assert resp['Location'] == redirect_url


@ddt.ddt
class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
Expand Down Expand Up @@ -2624,34 +2637,6 @@ def course_options(self):
return options


class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ModuleStoreTestCase):
"""
Ensure that the Enterprise Data Consent redirects are in place only when consent is required.
"""
def setUp(self):
super().setUp()
self.user = UserFactory.create()
assert self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.course = CourseFactory.create()
CourseOverview.load_from_module_store(self.course.id)
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)

@patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
def test_consent_required(self, mock_enterprise_customer_for_request):
"""
Test that enterprise data sharing consent is required when enabled for the various courseware views.
"""
# ENT-924: Temporary solution to replace sensitive SSO usernames.
mock_enterprise_customer_for_request.return_value = None

course_id = str(self.course.id)
for url in (
reverse("progress", kwargs=dict(course_id=course_id)),
reverse("student_progress", kwargs=dict(course_id=course_id, student_id=str(self.user.id))),
):
self.verify_consent_required(self.client, url) # lint-amnesty, pylint: disable=no-value-for-parameter


@ddt.ddt
class AccessUtilsTestCase(ModuleStoreTestCase):
"""
Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/courseware/views/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
from xmodule.modulestore.django import modulestore

from common.djangoapps.util.views import ensure_valid_course_key
from lms.djangoapps.courseware.decorators import courseware_view_hooks
from lms.djangoapps.courseware.exceptions import Redirect
from lms.djangoapps.courseware.masquerade import setup_masquerade
from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
from openedx.features.enterprise_support.api import data_sharing_consent_required

from ..block_render import get_block_for_descriptor
from ..courses import get_course_with_access
Expand All @@ -44,7 +44,7 @@ def enable_unenrolled_access(self):
@method_decorator(ensure_csrf_cookie)
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
@method_decorator(ensure_valid_course_key)
@method_decorator(data_sharing_consent_required)
@method_decorator(courseware_view_hooks)
def get(self, request, course_id, section=None, subsection=None, position=None):
"""
Instead of loading the legacy courseware sequences pages, load the equivalent URL
Expand Down
6 changes: 3 additions & 3 deletions lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
sort_by_start_date
)
from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
from lms.djangoapps.courseware.decorators import courseware_view_hooks
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect
from lms.djangoapps.courseware.masquerade import is_masquerading_as_specific_student, setup_masquerade
from lms.djangoapps.courseware.model_data import FieldDataCache
Expand Down Expand Up @@ -152,7 +153,6 @@
)
from openedx.features.course_experience.utils import dates_banner_should_display
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
from openedx.features.enterprise_support.api import data_sharing_consent_required

from ..block_render import get_block, get_block_by_usage_id, get_block_for_descriptor
from ..tabs import _get_dynamic_tabs
Expand Down Expand Up @@ -530,7 +530,7 @@ class CourseTabView(EdxFragmentView):
"""
@method_decorator(ensure_csrf_cookie)
@method_decorator(ensure_valid_course_key)
@method_decorator(data_sharing_consent_required)
@method_decorator(courseware_view_hooks)
def get(self, request, course_id, tab_type, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
"""
Displays a course tab page that contains a web fragment.
Expand Down Expand Up @@ -979,7 +979,7 @@ def dates(request, course_id):
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@ensure_valid_course_key
@data_sharing_consent_required
@courseware_view_hooks
def progress(request, course_id, student_id=None):
""" Display the progress page. """
course_key = CourseKey.from_string(course_id)
Expand Down
Loading
Loading