Merge branch 'master' into marco/remove-non-profit

This commit is contained in:
Awais Jibran
2021-10-14 12:17:56 +05:00
committed by GitHub
19 changed files with 239 additions and 178 deletions

View File

@@ -38,14 +38,6 @@ class ActivateLoginTest(LoginEnrollmentTestCase):
"""
self.logout()
def test_request_attr_on_logout(self):
"""
Test request object after logging out to see whether it
has 'is_from_log_out' attribute set to true.
"""
response = self.client.get(reverse('logout'))
assert getattr(response.wsgi_request, 'is_from_logout', False)
class PageLoaderTestCase(LoginEnrollmentTestCase):
"""

View File

@@ -991,6 +991,9 @@ COURSE_MESSAGE_ALERT_DURATION_IN_DAYS = 14
MARKETING_EMAILS_OPT_IN = False
# VAN-754 - Year of birth field put behind a flag to make it available for OpenedX.
COLLECT_YEAR_OF_BIRTH = True
############################# SET PATH INFORMATION #############################
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms
REPO_ROOT = PROJECT_ROOT.dirname()

View File

@@ -157,6 +157,12 @@ SESSION_COOKIE_HTTPONLY = ENV_TOKENS.get('SESSION_COOKIE_HTTPONLY', True)
DCS_SESSION_COOKIE_SAMESITE = ENV_TOKENS.get('DCS_SESSION_COOKIE_SAMESITE', DCS_SESSION_COOKIE_SAMESITE)
DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL = ENV_TOKENS.get('DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL', DCS_SESSION_COOKIE_SAMESITE_FORCE_ALL) # lint-amnesty, pylint: disable=line-too-long
# As django-cookies-samesite package is set to be removed from base requirements when we upgrade to Django 3.2,
# we should follow the settings name provided by Django.
# https://docs.djangoproject.com/en/3.2/ref/settings/#session-cookie-samesite
if django.VERSION >= (3, 1):
SESSION_COOKIE_SAMESITE = DCS_SESSION_COOKIE_SAMESITE
AWS_SES_REGION_NAME = ENV_TOKENS.get('AWS_SES_REGION_NAME', 'us-east-1')
AWS_SES_REGION_ENDPOINT = ENV_TOKENS.get('AWS_SES_REGION_ENDPOINT', 'email.us-east-1.amazonaws.com')

View File

@@ -33,40 +33,43 @@ ProviderExternalLinks = namedtuple(
['learn_more', 'configuration', 'general', 'accessibility', 'contact_email']
)
ProviderFeature = namedtuple('ProviderFeature', ['id', 'feature_support_type'])
class Features(Enum):
"""
Features to be used/mapped in discussion providers
"""
ANONYMOUS_POSTING = 'anonymous-posting'
# Todo: https://openedx.atlassian.net/browse/TNL-8546
# This will be added back in once we add LTI v1.3 support for discussion
# configuration in the future. https://openedx.atlassian.net/browse/TNL-8365
# AUTOMATIC_LEARNER_ENROLLMENT = 'automatic-learner-enrollment'
# Basic Supported Features
PRIMARY_DISCUSSION_APP_EXPERIENCE = ProviderFeature('primary-discussion-app-experience', 'basic')
LTI_BASIC_CONFIGURATION = ProviderFeature('lti-basic-configuration', 'basic')
# DISCUSSION_PAGE = ProviderFeature('discussion-page', 'basic')
BLACKOUT_DISCUSSION_DATES = 'blackout-discussion-dates'
COMMUNITY_TA_SUPPORT = 'community-ta-support'
COURSE_COHORT_SUPPORT = 'course-cohort-support'
DISCUSSION_PAGE = 'discussion-page'
INTERNATIONALIZATION_SUPPORT = 'internationalization-support'
PRIMARY_DISCUSSION_APP_EXPERIENCE = 'primary-discussion-app-experience'
QUESTION_DISCUSSION_SUPPORT = 'question-discussion-support'
REPORT_FLAG_CONTENT_TO_MODERATORS = 'report/flag-content-to-moderators'
RESEARCH_DATA_EVENTS = 'research-data-events'
WCAG_2_0_SUPPORT = 'wcag-2.0-support'
WCAG_2_1 = 'wcag-2.1'
ADVANCED_IN_CONTEXT_DISCUSSION = 'advanced-in-context-discussion'
DIRECT_MESSAGES_FROM_INSTRUCTORS = 'direct-messages-from-instructors'
DISCUSSION_CONTENT_PROMPTS = 'discussion-content-prompts'
EMAIL_NOTIFICATIONS = 'email-notifications'
EMBEDDED_COURSE_SECTIONS = 'embedded-course-sections'
GRADED_DISCUSSIONS = 'graded-discussions'
IN_PLATFORM_NOTIFICATIONS = 'in-platform-notifications'
LTI_ADVANCED_SHARING_MODE = 'lti-advanced-sharing-mode'
LTI_BASIC_CONFIGURATION = 'lti-basic-configuration'
SIMPLIFIED_IN_CONTEXT_DISCUSSION = 'simplified-in-context-discussion'
USER_MENTIONS = 'user-mentions'
# Partially Supported Features
QUESTION_DISCUSSION_SUPPORT = ProviderFeature('question-discussion-support', 'partial')
COMMUNITY_TA_SUPPORT = ProviderFeature('community-ta-support', 'partial')
REPORT_FLAG_CONTENT_TO_MODERATORS = ProviderFeature('report/flag-content-to-moderators', 'partial')
LTI_ADVANCED_SHARING_MODE = ProviderFeature('lti-advanced-sharing-mode', 'partial')
AUTOMATIC_LEARNER_ENROLLMENT = ProviderFeature('automatic-learner-enrollment', 'partial')
ANONYMOUS_POSTING = ProviderFeature('anonymous-posting', 'partial')
INTERNATIONALIZATION_SUPPORT = ProviderFeature('internationalization-support', 'partial')
EMAIL_NOTIFICATIONS = ProviderFeature('email-notifications', 'partial')
WCAG_2_0_SUPPORT = ProviderFeature('wcag-2.0-support', 'partial')
BLACKOUT_DISCUSSION_DATES = ProviderFeature('blackout-discussion-dates', 'partial')
# WCAG_2_1 = ProviderFeature('wcag-2.1', 'partial')
# EMBEDDED_COURSE_SECTIONS = ProviderFeature('embedded-course-sections', 'basic')
# Fully Supported Features
COURSE_COHORT_SUPPORT = ProviderFeature('course-cohort-support', 'full')
RESEARCH_DATA_EVENTS = ProviderFeature('research-data-events', 'full')
# Commonly Requested Features
IN_PLATFORM_NOTIFICATIONS = ProviderFeature('in-platform-notifications', 'common')
DISCUSSION_CONTENT_PROMPTS = ProviderFeature('discussion-content-prompts', 'common')
GRADED_DISCUSSIONS = ProviderFeature('graded-discussions', 'common')
DIRECT_MESSAGES_FROM_INSTRUCTORS = ProviderFeature('direct-messages-from-instructors', 'common')
USER_MENTIONS = ProviderFeature('user-mentions', 'common')
def pii_sharing_required_message(provider_name):
@@ -91,18 +94,18 @@ def pii_sharing_required_message(provider_name):
AVAILABLE_PROVIDER_MAP = {
'legacy': {
'features': [
Features.ANONYMOUS_POSTING.value,
Features.BLACKOUT_DISCUSSION_DATES.value,
Features.COMMUNITY_TA_SUPPORT.value,
Features.COURSE_COHORT_SUPPORT.value,
Features.DISCUSSION_PAGE.value,
Features.INTERNATIONALIZATION_SUPPORT.value,
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value,
Features.QUESTION_DISCUSSION_SUPPORT.value,
Features.RESEARCH_DATA_EVENTS.value,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
Features.WCAG_2_0_SUPPORT.value,
Features.WCAG_2_1.value,
Features.LTI_BASIC_CONFIGURATION.value.id,
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value.id,
Features.QUESTION_DISCUSSION_SUPPORT.value.id,
Features.COMMUNITY_TA_SUPPORT.value.id,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value.id,
Features.AUTOMATIC_LEARNER_ENROLLMENT.value.id,
Features.ANONYMOUS_POSTING.value.id,
Features.INTERNATIONALIZATION_SUPPORT.value.id,
Features.WCAG_2_0_SUPPORT.value.id,
Features.BLACKOUT_DISCUSSION_DATES.value.id,
Features.COURSE_COHORT_SUPPORT.value.id,
Features.RESEARCH_DATA_EVENTS.value.id,
],
'external_links': ProviderExternalLinks(
learn_more='',
@@ -114,20 +117,71 @@ AVAILABLE_PROVIDER_MAP = {
'messages': [],
'has_full_support': True
},
'ed-discuss': {
'features': [
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value.id,
Features.LTI_BASIC_CONFIGURATION.value.id,
Features.QUESTION_DISCUSSION_SUPPORT.value.id,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value.id,
Features.LTI_ADVANCED_SHARING_MODE.value.id,
Features.AUTOMATIC_LEARNER_ENROLLMENT.value.id,
Features.ANONYMOUS_POSTING.value.id,
Features.INTERNATIONALIZATION_SUPPORT.value.id,
Features.EMAIL_NOTIFICATIONS.value.id,
Features.WCAG_2_0_SUPPORT.value.id,
Features.BLACKOUT_DISCUSSION_DATES.value.id,
Features.IN_PLATFORM_NOTIFICATIONS.value.id,
Features.USER_MENTIONS.value.id,
],
'external_links': ProviderExternalLinks(
learn_more='',
configuration='',
general='https://edstem.org/us/',
accessibility='',
contact_email='',
)._asdict(),
'messages': [],
'has_full_support': False
},
'inscribe': {
'features': [
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value.id,
Features.LTI_BASIC_CONFIGURATION.value.id,
Features.QUESTION_DISCUSSION_SUPPORT.value.id,
Features.COMMUNITY_TA_SUPPORT.value.id,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value.id,
Features.LTI_ADVANCED_SHARING_MODE.value.id,
Features.AUTOMATIC_LEARNER_ENROLLMENT.value.id,
Features.ANONYMOUS_POSTING.value.id,
Features.INTERNATIONALIZATION_SUPPORT.value.id,
Features.EMAIL_NOTIFICATIONS.value.id,
Features.WCAG_2_0_SUPPORT.value.id,
Features.RESEARCH_DATA_EVENTS.value.id,
Features.IN_PLATFORM_NOTIFICATIONS.value.id,
Features.DISCUSSION_CONTENT_PROMPTS.value.id,
],
'external_links': ProviderExternalLinks(
learn_more='',
configuration='',
general='https://www.inscribeapp.com/',
accessibility='',
contact_email='',
)._asdict(),
'messages': [pii_sharing_required_message('InScribe')],
'has_full_support': False
},
'piazza': {
'features': [
Features.ANONYMOUS_POSTING.value,
Features.BLACKOUT_DISCUSSION_DATES.value,
Features.COMMUNITY_TA_SUPPORT.value,
Features.DIRECT_MESSAGES_FROM_INSTRUCTORS.value,
Features.DISCUSSION_CONTENT_PROMPTS.value,
Features.DISCUSSION_PAGE.value,
Features.EMAIL_NOTIFICATIONS.value,
Features.LTI_BASIC_CONFIGURATION.value,
Features.QUESTION_DISCUSSION_SUPPORT.value,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
Features.USER_MENTIONS.value,
Features.WCAG_2_0_SUPPORT.value,
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value.id,
Features.LTI_BASIC_CONFIGURATION.value.id,
Features.QUESTION_DISCUSSION_SUPPORT.value.id,
Features.COMMUNITY_TA_SUPPORT.value.id,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value.id,
Features.LTI_ADVANCED_SHARING_MODE.value.id,
Features.ANONYMOUS_POSTING.value.id,
Features.EMAIL_NOTIFICATIONS.value.id,
Features.WCAG_2_0_SUPPORT.value.id,
Features.BLACKOUT_DISCUSSION_DATES.value.id,
],
'external_links': ProviderExternalLinks(
learn_more='https://piazza.com/product/overview',
@@ -141,19 +195,18 @@ AVAILABLE_PROVIDER_MAP = {
},
'yellowdig': {
'features': [
Features.ANONYMOUS_POSTING.value,
Features.COMMUNITY_TA_SUPPORT.value,
Features.DIRECT_MESSAGES_FROM_INSTRUCTORS.value,
Features.EMAIL_NOTIFICATIONS.value,
Features.GRADED_DISCUSSIONS.value,
Features.IN_PLATFORM_NOTIFICATIONS.value,
Features.LTI_BASIC_CONFIGURATION.value,
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value,
Features.QUESTION_DISCUSSION_SUPPORT.value,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
Features.RESEARCH_DATA_EVENTS.value,
Features.USER_MENTIONS.value,
Features.WCAG_2_0_SUPPORT.value,
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value.id,
Features.LTI_BASIC_CONFIGURATION.value.id,
Features.QUESTION_DISCUSSION_SUPPORT.value.id,
Features.COMMUNITY_TA_SUPPORT.value.id,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value.id,
Features.EMAIL_NOTIFICATIONS.value.id,
Features.WCAG_2_0_SUPPORT.value.id,
Features.RESEARCH_DATA_EVENTS.value.id,
Features.IN_PLATFORM_NOTIFICATIONS.value.id,
Features.GRADED_DISCUSSIONS.value.id,
Features.DIRECT_MESSAGES_FROM_INSTRUCTORS.value.id,
Features.USER_MENTIONS.value.id,
],
'external_links': ProviderExternalLinks(
learn_more='https://www.youtube.com/watch?v=ZACief-qMwY',
@@ -166,59 +219,6 @@ AVAILABLE_PROVIDER_MAP = {
'has_full_support': False,
'admin_only_config': True,
},
'inscribe': {
'features': [
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value,
Features.LTI_BASIC_CONFIGURATION.value,
],
'external_links': ProviderExternalLinks(
learn_more='',
configuration='',
general='https://www.inscribeapp.com/',
accessibility='',
contact_email='',
)._asdict(),
'messages': [pii_sharing_required_message('InScribe')],
'has_full_support': False
},
'discourse': {
'features': [
Features.LTI_ADVANCED_SHARING_MODE.value,
Features.LTI_BASIC_CONFIGURATION.value,
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value,
],
'external_links': ProviderExternalLinks(
learn_more='',
configuration='',
general='http://discourse.org/',
accessibility='',
contact_email='',
)._asdict(),
'messages': [pii_sharing_required_message('Discourse')],
'has_full_support': False
},
'ed-discuss': {
'features': [
Features.ANONYMOUS_POSTING.value,
Features.COMMUNITY_TA_SUPPORT.value,
Features.EMAIL_NOTIFICATIONS.value,
Features.INTERNATIONALIZATION_SUPPORT.value,
Features.LTI_BASIC_CONFIGURATION.value,
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value,
Features.QUESTION_DISCUSSION_SUPPORT.value,
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
Features.WCAG_2_0_SUPPORT.value,
],
'external_links': ProviderExternalLinks(
learn_more='',
configuration='',
general='https://edstem.org/us/',
accessibility='',
contact_email='',
)._asdict(),
'messages': [],
'has_full_support': False
}
}

View File

@@ -203,7 +203,7 @@ class DiscussionsConfigurationSerializer(serializers.ModelSerializer):
course_key = instance.context_key
payload = super().to_representation(instance)
lti_configuration_data = {}
supports_lti = instance.supports(Features.LTI_BASIC_CONFIGURATION.value)
supports_lti = instance.supports(Features.LTI_BASIC_CONFIGURATION.value.id)
if supports_lti:
lti_configuration = LtiSerializer(instance.lti_configuration, context={
'pii_sharing_allowed': get_lti_pii_sharing_state_for_course(course_key),
@@ -219,7 +219,10 @@ class DiscussionsConfigurationSerializer(serializers.ModelSerializer):
)
if legacy_settings.is_valid(raise_exception=True):
plugin_configuration = legacy_settings.data
features_list = [feature.value for feature in Features]
features_list = [
{'id': feature.value.id, 'feature_support_type': feature.value.feature_support_type}
for feature in Features
]
payload.update({
'features': features_list,
'lti_configuration': lti_configuration_data,
@@ -256,7 +259,7 @@ class DiscussionsConfigurationSerializer(serializers.ModelSerializer):
Update LtiConfiguration
"""
lti_configuration_data = validated_data.get('lti_configuration')
supports_lti = instance.supports(Features.LTI_BASIC_CONFIGURATION.value)
supports_lti = instance.supports(Features.LTI_BASIC_CONFIGURATION.value.id)
if not supports_lti:
instance.lti_configuration = None
elif lti_configuration_data:

View File

@@ -150,7 +150,12 @@ class DataTest(AuthorizedApiTest):
name for name, spec in data['providers']['available'].items()
if "messages" not in spec
], "Found available providers without messages field"
assert data['lti_configuration'] == {}
assert data['lti_configuration'] == {
'lti_1p1_client_key': '',
'lti_1p1_client_secret': '',
'lti_1p1_launch_url': '',
'version': None
}
assert data['plugin_configuration'] == {
'allow_anonymous': True,
'allow_anonymous_to_peers': False,
@@ -480,7 +485,6 @@ class DataTest(AuthorizedApiTest):
assert data['enabled']
assert data['provider_type'] == 'legacy'
assert not data['plugin_configuration']['allow_anonymous']
assert not data['lti_configuration']
@ddt.data(*[
user_type.name for user_type in CourseUserType

View File

@@ -3,6 +3,17 @@ This module defines SafeSessionMiddleware that makes use of a
SafeCookieData that cryptographically binds the user to the session id
in the cookie.
The primary goal is to avoid and detect situations where a session is
corrupted and the client becomes logged in as the wrong user. This
could happen via cache corruption (which we've seen before) or a
request handling bug. It's unlikely to happen again, but would be a
critical issue, so we've built in some checks to make sure the user on
the session doesn't change over the course of the session or between
the request and response phases.
The secondary goal is to improve upon Django's session handling by
including cryptographically enforced expiration.
The implementation is inspired in part by the proposal in the paper
<http://www.cse.msu.edu/~alexliu/publications/Cookie/cookie.pdf>
but deviates in a number of ways; mostly it just uses the technique
@@ -66,9 +77,8 @@ Custom Attributes:
import inspect
from base64 import b64encode
from contextlib import contextmanager
from hashlib import sha256
from logging import ERROR, getLogger
from logging import getLogger
from django.conf import settings
from django.contrib.auth import SESSION_KEY
@@ -278,7 +288,8 @@ class SafeSessionMiddleware(SessionMiddleware, MiddlewareMixin):
Step 4. Once the session is retrieved, verify that the user
bound in the safe_cookie_data matches the user attached to the
server's session information.
server's session information. Otherwise, reject the request
(bypass the view and return an error or redirect).
Step 5. If all is successful, the now verified user_id is stored
separately in the request object so it is available for another
@@ -313,6 +324,8 @@ class SafeSessionMiddleware(SessionMiddleware, MiddlewareMixin):
if LOG_REQUEST_USER_CHANGES:
log_request_user_changes(request)
else:
# Return an error or redirect, and don't continue to
# the underlying view.
return self._on_user_authentication_failed(request)
def process_response(self, request, response):
@@ -345,13 +358,12 @@ class SafeSessionMiddleware(SessionMiddleware, MiddlewareMixin):
if not _is_cookie_marked_for_deletion(request) and _is_cookie_present(response):
try:
user_id_in_session = self.get_user_id_from_session(request)
with controlled_logging(request, log):
self._verify_user(request, user_id_in_session) # Step 2
self._verify_user(request, response, user_id_in_session) # Step 2
# Use the user_id marked in the session instead of the
# one in the request in case the user is not set in the
# request, for example during Anonymous API access.
self.update_with_safe_session_cookie(response.cookies, user_id_in_session) # Step 3
# Use the user_id marked in the session instead of the
# one in the request in case the user is not set in the
# request, for example during Anonymous API access.
self.update_with_safe_session_cookie(response.cookies, user_id_in_session) # Step 3
except SafeCookieError:
_mark_cookie_for_deletion(request)
@@ -378,12 +390,23 @@ class SafeSessionMiddleware(SessionMiddleware, MiddlewareMixin):
return redirect_to_login(request.path)
@staticmethod
def _verify_user(request, userid_in_session):
def _verify_user(request, response, userid_in_session):
"""
Logs an error if the user marked at the time of process_request
does not match either the current user in the request or the
given userid_in_session.
"""
# It's expected that a small number of views may change the
# user over the course of the request. We have exemptions for
# the user changing to/from None, but the login view can
# sometimes change the user from one value to another between
# the request and response phases, specifically when the login
# page is used during an active session.
#
# The relevant views set a flag to indicate the exemption.
if getattr(response, 'safe_sessions_expected_user_change', None):
return
if hasattr(request, 'safe_cookie_verified_user_id'):
if hasattr(request.user, 'real_user'):
# If a view overrode the request.user with a masqueraded user, this will
@@ -431,6 +454,7 @@ class SafeSessionMiddleware(SessionMiddleware, MiddlewareMixin):
except KeyError:
return None
# TODO move to test code, maybe rename, get rid of old Django compat stuff
@staticmethod
def set_user_id_in_session(request, user):
"""
@@ -573,28 +597,13 @@ def log_request_user_changes(request):
request.__class__ = SafeSessionRequestWrapper
def _is_from_logout(request):
def mark_user_change_as_expected(response, new_user_id):
"""
Returns whether the request has come from logout action to see if
'is_from_logout' attribute is present.
"""
return getattr(request, 'is_from_logout', False)
Indicate to the safe-sessions middleware that it is expected that
the user is changing between the request and response phase of
the current request.
@contextmanager
def controlled_logging(request, logger):
The new_user_id may be None or an LMS user ID, and may be the same
as the previous user ID.
"""
Control the logging by changing logger's level if
the request is from logout.
"""
default_level = None
from_logout = _is_from_logout(request)
if from_logout:
default_level = logger.getEffectiveLevel()
logger.setLevel(ERROR)
try:
yield
finally:
if from_logout:
logger.setLevel(default_level)
response.safe_sessions_expected_user_change = {'new_user_id': new_user_id}

View File

@@ -17,7 +17,7 @@ from django.test.utils import override_settings
from openedx.core.djangolib.testing.utils import get_mock_request
from common.djangoapps.student.tests.factories import UserFactory
from ..middleware import SafeCookieData, SafeSessionMiddleware, log_request_user_changes
from ..middleware import SafeCookieData, SafeSessionMiddleware, log_request_user_changes, mark_user_change_as_expected
from .test_utils import TestSafeSessionsLogMixin
@@ -263,9 +263,9 @@ class TestSafeSessionMiddleware(TestSafeSessionsLogMixin, TestCase):
settings.SESSION_COOKIE_NAME
]
def verify_success(self):
def set_up_for_success(self):
"""
Verifies success path.
Set up request for success path -- everything up until process_response().
"""
self.client.login(username=self.user.username, password='test')
self.request.user = self.user
@@ -281,6 +281,12 @@ class TestSafeSessionMiddleware(TestSafeSessionsLogMixin, TestCase):
assert self.request.safe_cookie_verified_user_id == self.user.id
self.cookies_from_request_to_response()
def verify_success(self):
"""
Verifies success path.
"""
self.set_up_for_success()
with self.assert_not_logged():
response = SafeSessionMiddleware().process_response(self.request, self.client.response)
assert response.status_code == 200
@@ -322,6 +328,41 @@ class TestSafeSessionMiddleware(TestSafeSessionsLogMixin, TestCase):
self.request.META = {'HTTP_USER_AGENT': 'open edX Mobile App Version 2.1'}
self.verify_error(401)
def test_warn_on_user_change(self):
"""
Verifies that warnings are emitted and custom attributes set if
the user changes unexpectedly between request and response.
"""
self.set_up_for_success()
# But then user changes unexpectedly
self.request.user = UserFactory.create()
with self.assert_logged_for_request_user_mismatch(self.user.id, self.request.user.id, 'warning', '/'):
with patch('openedx.core.djangoapps.safe_sessions.middleware.set_custom_attribute') as mock_attr:
response = SafeSessionMiddleware().process_response(self.request, self.client.response)
assert response.status_code == 200
mock_attr.assert_called_with("safe_sessions.user_mismatch", "request-response-mismatch")
def test_no_warn_on_expected_user_change(self):
"""
Verifies that no warnings is emitted when the user change is expected.
This might happen on a login, for example.
"""
self.set_up_for_success()
# User changes...
new_user = UserFactory.create()
self.request.user = new_user
# ...but so does session, and view sets a flag to say it's OK.
mark_user_change_as_expected(self.client.response, new_user.id)
with self.assert_no_warning_logged():
with patch('openedx.core.djangoapps.safe_sessions.middleware.set_custom_attribute') as mock_attr:
response = SafeSessionMiddleware().process_response(self.request, self.client.response)
assert response.status_code == 200
mock_attr.assert_not_called()
@ddt.ddt
class TestLogRequestUserChanges(TestCase):

View File

@@ -41,6 +41,7 @@ from common.djangoapps.track import segment
from common.djangoapps.util.json_request import JsonResponse
from common.djangoapps.util.password_policy_validators import normalize_password
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
from openedx.core.djangoapps.safe_sessions.middleware import mark_user_change_as_expected
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_authn.config.waffle import ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY
from openedx.core.djangoapps.user_authn.cookies import get_response_with_refreshed_jwt_cookies, set_logged_in_cookies
@@ -593,6 +594,7 @@ def login_user(request, api_version='v1'):
set_custom_attribute('login_user_auth_failed_error', False)
set_custom_attribute('login_user_response_status', response.status_code)
set_custom_attribute('login_user_redirect_url', redirect_url)
mark_user_change_as_expected(response, user.id)
return response
except AuthFailedError as error:
response_content = error.get_response()

View File

@@ -11,6 +11,7 @@ from django.utils.http import urlencode
from django.views.generic import TemplateView
from oauth2_provider.models import Application
from openedx.core.djangoapps.safe_sessions.middleware import mark_user_change_as_expected
from openedx.core.djangoapps.user_authn.cookies import delete_logged_in_cookies
from openedx.core.djangoapps.user_authn.utils import is_safe_login_or_logout_redirect
from common.djangoapps.third_party_auth import pipeline as tpa_pipeline
@@ -69,7 +70,6 @@ class LogoutView(TemplateView):
def dispatch(self, request, *args, **kwargs):
# We do not log here, because we have a handler registered to perform logging on successful logouts.
request.is_from_logout = True
# Get third party auth provider's logout url
self.tpa_logout_url = tpa_pipeline.get_idp_logout_url_from_running_pipeline(request)
@@ -81,6 +81,7 @@ class LogoutView(TemplateView):
# Clear the cookie used by the edx.org marketing site
delete_logged_in_cookies(response)
mark_user_change_as_expected(response, None)
return response
def _build_logout_url(self, url):

View File

@@ -385,7 +385,6 @@ def _track_user_registration(user, profile, params, third_party_provider, regist
# VAN-738 - added below properties to experiment marketing emails opt in/out events on Braze.
if params.get('marketing_emails_opt_in') and settings.MARKETING_EMAILS_OPT_IN:
properties['marketing_emails_opt_in'] = params.get('marketing_emails_opt_in') == 'true'
properties['is_active'] = params.get('marketing_emails_opt_in') == 'true'
# DENG-803: For segment events forwarded along to Hubspot, duplicate the `properties` section of
# the event payload into the `traits` section so that they can be received. This is a temporary

View File

@@ -30,7 +30,7 @@ django-storages<1.9
# The team that owns this package will manually bump this package rather than having it pulled in automatically.
# This is to allow them to better control its deployment and to do it in a process that works better
# for them.
edx-enterprise==3.30.6
edx-enterprise==3.30.10
# Newer versions need a more recent version of python-dateutil
freezegun==0.3.12

View File

@@ -433,7 +433,7 @@ edx-drf-extensions==8.0.0
# edx-rbac
# edx-when
# edxval
edx-enterprise==3.30.6
edx-enterprise==3.30.10
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.in
@@ -462,7 +462,7 @@ edx-opaque-keys[django]==2.2.2
# xmodule
edx-organizations==6.10.0
# via -r requirements/edx/base.in
edx-proctoring==4.1.1
edx-proctoring==4.1.2
# via
# -r requirements/edx/base.in
# edx-proctoring-proctortrack

View File

@@ -535,7 +535,7 @@ edx-drf-extensions==8.0.0
# edx-rbac
# edx-when
# edxval
edx-enterprise==3.30.6
edx-enterprise==3.30.10
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/testing.txt
@@ -568,7 +568,7 @@ edx-opaque-keys[django]==2.2.2
# xmodule
edx-organizations==6.10.0
# via -r requirements/edx/testing.txt
edx-proctoring==4.1.1
edx-proctoring==4.1.2
# via
# -r requirements/edx/testing.txt
# edx-proctoring-proctortrack

View File

@@ -10,7 +10,7 @@ click==7.1.2
# pip-tools
pep517==0.11.0
# via pip-tools
pip-tools==6.3.0
pip-tools==6.4.0
# via -r requirements/edx/pip-tools.in
tomli==1.2.1
# via pep517

View File

@@ -515,7 +515,7 @@ edx-drf-extensions==8.0.0
# edx-rbac
# edx-when
# edxval
edx-enterprise==3.30.6
edx-enterprise==3.30.10
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -549,7 +549,7 @@ edx-opaque-keys[django]==2.2.2
# xmodule
edx-organizations==6.10.0
# via -r requirements/edx/base.txt
edx-proctoring==4.1.1
edx-proctoring==4.1.2
# via
# -r requirements/edx/base.txt
# edx-proctoring-proctortrack

View File

@@ -48,6 +48,7 @@ source $VENV_PATH/edx-venv/bin/activate
# Hack to fix up egg-link files given that the virtualenv is not relocatable
sed -i "s|^/home/jenkins/shallow-clone|`pwd`|" -- \
$VENV_PATH/edx-venv/lib/python*/site-packages/*.egg-link
pip install pip==21.3
pip install -qr requirements/edx/pip-tools.txt
pip-sync -q requirements/edx/testing.txt requirements/edx/django.txt

View File

@@ -1,6 +1,6 @@
<%page expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<div class="accomplishment-metadata-bit accomplishment-metadata-aboutedx">
@@ -8,7 +8,7 @@ from openedx.core.djangolib.markup import HTML, Text
<h3 class="accomplishment-metadata-title">${_("About edX")}</h3>
<p class="accomplishment-metadata-copy">
## Translators: This string will not be used in Open edX installations.
${Text(_("{link_start}edX{link_end} offers interactive online classes and MOOCs from the world's best universities, including MIT, Harvard, Berkeley, University of Texas, and many others. edX is an online initiative created by founding partners Harvard and MIT.")).format(
${Text(_("{link_start}edX{link_end} offers interactive online classes and MOOCs from the world's best universities, including MIT, Harvard, Berkeley, University of Texas, and many others. edX is a mission driven initiative created by founding partners Harvard and MIT.")).format(
link_start=HTML('<a href="http://www.edx.org">'),
link_end=HTML('</a>'),
)}