Merge pull request #14053 from open-craft/haikuginger/final-consent-hooks
[ENT-61] Changes to accommodate consent logic on Enterprise app
This commit is contained in:
@@ -188,6 +188,19 @@ class AccountCreationForm(forms.Form):
|
||||
"required": _("To enroll, you must follow the honor code.")
|
||||
}
|
||||
)
|
||||
elif field_name == 'data_sharing_consent':
|
||||
if field_value == "required":
|
||||
self.fields[field_name] = TrueField(
|
||||
error_messages={
|
||||
"required": _(
|
||||
"You must consent to data sharing to register."
|
||||
)
|
||||
}
|
||||
)
|
||||
elif field_value == 'optional':
|
||||
self.fields[field_name] = forms.BooleanField(
|
||||
required=False
|
||||
)
|
||||
else:
|
||||
required = field_value == "required"
|
||||
min_length = 1 if field_name in ("gender", "level_of_education") else 2
|
||||
|
||||
@@ -46,6 +46,7 @@ from social.exceptions import AuthException, AuthAlreadyAssociated
|
||||
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from util.enterprise_helpers import data_sharing_consent_requirement_at_login
|
||||
from course_modes.models import CourseMode
|
||||
from shoppingcart.api import order_history
|
||||
from student.models import (
|
||||
@@ -1644,6 +1645,10 @@ def create_account_with_params(request, params):
|
||||
if should_link_with_social_auth or (third_party_auth.is_enabled() and pipeline.running(request)):
|
||||
params["password"] = pipeline.make_random_password()
|
||||
|
||||
# Add a form requirement for data sharing consent if the EnterpriseCustomer
|
||||
# for the request requires it at login
|
||||
extra_fields['data_sharing_consent'] = data_sharing_consent_requirement_at_login(request)
|
||||
|
||||
# if doing signup for an external authorization, then get email, password, name from the eamap
|
||||
# don't use the ones from the form, since the user could have hacked those
|
||||
# unless originally we didn't get a valid email or name from the external auth
|
||||
@@ -1740,6 +1745,9 @@ def create_account_with_params(request, params):
|
||||
if third_party_auth.is_enabled() and pipeline.running(request):
|
||||
running_pipeline = pipeline.get(request)
|
||||
third_party_provider = provider.Registry.get_from_pipeline(running_pipeline)
|
||||
# Store received data sharing consent field values in the pipeline for use
|
||||
# by any downstream pipeline elements which require them.
|
||||
running_pipeline['kwargs']['data_sharing_consent'] = form.cleaned_data.get('data_sharing_consent', None)
|
||||
|
||||
# Track the user's registration
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
|
||||
@@ -23,3 +23,34 @@ class ExceptionMiddleware(SocialAuthExceptionMiddleware):
|
||||
redirect_uri = pipeline.AUTH_DISPATCH_URLS[auth_entry]
|
||||
|
||||
return redirect_uri
|
||||
|
||||
|
||||
class PipelineQuarantineMiddleware(object):
|
||||
"""
|
||||
Middleware flushes the session if a user agent with a quarantined session
|
||||
attempts to leave the quarantined set of views.
|
||||
"""
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Check the session to see if we've quarantined the user to a particular
|
||||
step of the authentication pipeline; if so, look up which modules the
|
||||
user is allowed to browse to without breaking the pipeline. If the view
|
||||
that's been requested is outside those modules, then flush the session.
|
||||
|
||||
In general, this middleware should be used in cases where allowing the
|
||||
user to exit the running pipeline would be undesirable, and where it'd
|
||||
be better to flush the session state rather than allow it. Pipeline
|
||||
quarantining is utilized by the Enterprise application to enforce
|
||||
collection of user consent for sharing data with a linked third-party
|
||||
authentication provider.
|
||||
"""
|
||||
running_pipeline = request.session.get('partial_pipeline')
|
||||
|
||||
if not running_pipeline:
|
||||
return
|
||||
|
||||
view_module = view_func.__module__
|
||||
quarantined_modules = request.session.get('third_party_auth_quarantined_modules', None)
|
||||
if quarantined_modules is not None and not any(view_module.startswith(mod) for mod in quarantined_modules):
|
||||
request.session.flush()
|
||||
|
||||
@@ -74,6 +74,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from social.apps.django_app.default import models
|
||||
from social.apps.django_app.default.models import UserSocialAuth
|
||||
from social.exceptions import AuthException
|
||||
from social.pipeline import partial
|
||||
from social.pipeline.social_auth import associate_by_email
|
||||
@@ -200,6 +201,38 @@ def get(request):
|
||||
return request.session.get('partial_pipeline')
|
||||
|
||||
|
||||
def get_real_social_auth_object(request):
|
||||
"""
|
||||
At times, the pipeline will have a "social" kwarg that contains a dictionary
|
||||
rather than an actual DB-backed UserSocialAuth object. We need the real thing,
|
||||
so this method allows us to get that by passing in the relevant request.
|
||||
"""
|
||||
running_pipeline = get(request)
|
||||
if running_pipeline and 'social' in running_pipeline['kwargs']:
|
||||
social = running_pipeline['kwargs']['social']
|
||||
if isinstance(social, dict):
|
||||
social = UserSocialAuth.objects.get(uid=social.get('uid', ''))
|
||||
return social
|
||||
|
||||
|
||||
def quarantine_session(request, locations):
|
||||
"""
|
||||
Set a session variable indicating that the session is restricted
|
||||
to being used in views contained in the modules listed by string
|
||||
in the `locations` argument.
|
||||
|
||||
Example: ``quarantine_session(request, ('enterprise.views',))``
|
||||
"""
|
||||
request.session['third_party_auth_quarantined_modules'] = locations
|
||||
|
||||
|
||||
def lift_quarantine(request):
|
||||
"""
|
||||
Remove the session quarantine variable.
|
||||
"""
|
||||
request.session.pop('third_party_auth_quarantined_modules', None)
|
||||
|
||||
|
||||
def get_authenticated_user(auth_provider, username, uid):
|
||||
"""Gets a saved user authenticated by a particular backend.
|
||||
|
||||
|
||||
@@ -10,9 +10,12 @@ If true, it:
|
||||
b) calls apply_settings(), passing in the Django settings
|
||||
"""
|
||||
|
||||
from util.enterprise_helpers import insert_enterprise_pipeline_elements
|
||||
|
||||
_FIELDS_STORED_IN_SESSION = ['auth_entry', 'next']
|
||||
_MIDDLEWARE_CLASSES = (
|
||||
'third_party_auth.middleware.ExceptionMiddleware',
|
||||
'third_party_auth.middleware.PipelineQuarantineMiddleware',
|
||||
)
|
||||
_SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/dashboard'
|
||||
|
||||
@@ -37,7 +40,7 @@ def apply_settings(django_settings):
|
||||
|
||||
# Inject our customized auth pipeline. All auth backends must work with
|
||||
# this pipeline.
|
||||
django_settings.SOCIAL_AUTH_PIPELINE = (
|
||||
django_settings.SOCIAL_AUTH_PIPELINE = [
|
||||
'third_party_auth.pipeline.parse_query_params',
|
||||
'social.pipeline.social_auth.social_details',
|
||||
'social.pipeline.social_auth.social_uid',
|
||||
@@ -53,7 +56,10 @@ def apply_settings(django_settings):
|
||||
'social.pipeline.user.user_details',
|
||||
'third_party_auth.pipeline.set_logged_in_cookies',
|
||||
'third_party_auth.pipeline.login_analytics',
|
||||
)
|
||||
]
|
||||
|
||||
# Add enterprise pipeline elements if the enterprise app is installed
|
||||
insert_enterprise_pipeline_elements(django_settings.SOCIAL_AUTH_PIPELINE)
|
||||
|
||||
# Required so that we can use unmodified PSA OAuth2 backends:
|
||||
django_settings.SOCIAL_AUTH_STRATEGY = 'third_party_auth.strategy.ConfigurationModelStrategy'
|
||||
|
||||
@@ -76,15 +76,16 @@ class IntegrationTestMixin(object):
|
||||
self.assertEqual(form_fields['email']['defaultValue'], self.USER_EMAIL)
|
||||
self.assertEqual(form_fields['name']['defaultValue'], self.USER_NAME)
|
||||
self.assertEqual(form_fields['username']['defaultValue'], self.USER_USERNAME)
|
||||
registration_values = {
|
||||
'email': 'email-edited@tpa-test.none',
|
||||
'name': 'My Customized Name',
|
||||
'username': 'new_username',
|
||||
'honor_code': True,
|
||||
}
|
||||
# Now complete the form:
|
||||
ajax_register_response = self.client.post(
|
||||
reverse('user_api_registration'),
|
||||
{
|
||||
'email': 'email-edited@tpa-test.none',
|
||||
'name': 'My Customized Name',
|
||||
'username': 'new_username',
|
||||
'honor_code': True,
|
||||
}
|
||||
registration_values
|
||||
)
|
||||
self.assertEqual(ajax_register_response.status_code, 200)
|
||||
# Then the AJAX will finish the third party auth:
|
||||
|
||||
@@ -163,6 +163,7 @@ class TestShibIntegrationTest(IntegrationTestMixin, testutil.SAMLTestCase):
|
||||
def _configure_testshib_provider(self, **kwargs):
|
||||
""" Enable and configure the TestShib SAML IdP as a third_party_auth provider """
|
||||
fetch_metadata = kwargs.pop('fetch_metadata', True)
|
||||
assert_metadata_updates = kwargs.pop('assert_metadata_updates', True)
|
||||
kwargs.setdefault('name', self.PROVIDER_NAME)
|
||||
kwargs.setdefault('enabled', True)
|
||||
kwargs.setdefault('visible', True)
|
||||
@@ -176,9 +177,10 @@ class TestShibIntegrationTest(IntegrationTestMixin, testutil.SAMLTestCase):
|
||||
if fetch_metadata:
|
||||
self.assertTrue(httpretty.is_enabled())
|
||||
num_changed, num_failed, num_total = fetch_saml_metadata()
|
||||
self.assertEqual(num_failed, 0)
|
||||
self.assertEqual(num_changed, 1)
|
||||
self.assertEqual(num_total, 1)
|
||||
if assert_metadata_updates:
|
||||
self.assertEqual(num_failed, 0)
|
||||
self.assertEqual(num_changed, 1)
|
||||
self.assertEqual(num_total, 1)
|
||||
|
||||
def do_provider_login(self, provider_redirect_url):
|
||||
""" Mocked: the user logs in to TestShib and then gets redirected back """
|
||||
|
||||
51
common/djangoapps/third_party_auth/tests/test_middleware.py
Normal file
51
common/djangoapps/third_party_auth/tests/test_middleware.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Test the session-flushing middleware
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import Client
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestSessionFlushMiddleware(unittest.TestCase):
|
||||
"""
|
||||
Ensure that if the pipeline is exited when it's been quarantined,
|
||||
the entire session is flushed.
|
||||
"""
|
||||
def test_session_flush(self):
|
||||
"""
|
||||
Test that a quarantined session is flushed when navigating elsewhere
|
||||
"""
|
||||
client = Client()
|
||||
session = client.session
|
||||
session['fancy_variable'] = 13025
|
||||
session['partial_pipeline'] = 'pipeline_running'
|
||||
session['third_party_auth_quarantined_modules'] = ('fake_quarantined_module',)
|
||||
session.save()
|
||||
client.get('/')
|
||||
self.assertEqual(client.session.get('fancy_variable', None), None)
|
||||
|
||||
def test_session_no_running_pipeline(self):
|
||||
"""
|
||||
Test that a quarantined session without a running pipeline is not flushed
|
||||
"""
|
||||
client = Client()
|
||||
session = client.session
|
||||
session['fancy_variable'] = 13025
|
||||
session['third_party_auth_quarantined_modules'] = ('fake_quarantined_module',)
|
||||
session.save()
|
||||
client.get('/')
|
||||
self.assertEqual(client.session.get('fancy_variable', None), 13025)
|
||||
|
||||
def test_session_no_quarantine(self):
|
||||
"""
|
||||
Test that a session with a running pipeline but no quarantine is not flushed
|
||||
"""
|
||||
client = Client()
|
||||
session = client.session
|
||||
session['fancy_variable'] = 13025
|
||||
session['partial_pipeline'] = 'pipeline_running'
|
||||
session.save()
|
||||
client.get('/')
|
||||
self.assertEqual(client.session.get('fancy_variable', None), 13025)
|
||||
@@ -5,6 +5,7 @@ import unittest
|
||||
from django.conf import settings
|
||||
from django import test
|
||||
from django.contrib.auth import models
|
||||
import mock
|
||||
|
||||
from third_party_auth import pipeline, provider
|
||||
from third_party_auth.tests import testutil
|
||||
@@ -208,3 +209,95 @@ class UrlFormationTestCase(TestCase):
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
pipeline.get_complete_url(provider_id)
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
testutil.AUTH_FEATURES_KEY in settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES')
|
||||
class TestPipelineUtilityFunctions(TestCase, test.TestCase):
|
||||
"""
|
||||
Test some of the isolated utility functions in the pipeline
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestPipelineUtilityFunctions, self).setUp()
|
||||
self.user = social_models.DjangoStorage.user.create_user(username='username', password='password')
|
||||
self.social_auth = social_models.UserSocialAuth.objects.create(
|
||||
user=self.user,
|
||||
uid='fake uid',
|
||||
provider='fake provider'
|
||||
)
|
||||
|
||||
def test_get_real_social_auth_from_dict(self):
|
||||
"""
|
||||
Test that we can use a dictionary with a UID entry to retrieve a
|
||||
database-backed UserSocialAuth object.
|
||||
"""
|
||||
request = mock.MagicMock(
|
||||
session={
|
||||
'partial_pipeline': {
|
||||
'kwargs': {
|
||||
'social': {
|
||||
'uid': 'fake uid'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
real_social = pipeline.get_real_social_auth_object(request)
|
||||
self.assertEqual(real_social, self.social_auth)
|
||||
|
||||
def test_get_real_social_auth(self):
|
||||
"""
|
||||
Test that trying to get a database-backed UserSocialAuth from an existing
|
||||
instance returns correctly.
|
||||
"""
|
||||
request = mock.MagicMock(
|
||||
session={
|
||||
'partial_pipeline': {
|
||||
'kwargs': {
|
||||
'social': self.social_auth
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
real_social = pipeline.get_real_social_auth_object(request)
|
||||
self.assertEqual(real_social, self.social_auth)
|
||||
|
||||
def test_get_real_social_auth_no_pipeline(self):
|
||||
"""
|
||||
Test that if there's no running pipeline, we return None when looking
|
||||
for a database-backed UserSocialAuth object.
|
||||
"""
|
||||
request = mock.MagicMock(session={})
|
||||
real_social = pipeline.get_real_social_auth_object(request)
|
||||
self.assertEqual(real_social, None)
|
||||
|
||||
def test_get_real_social_auth_no_social(self):
|
||||
"""
|
||||
Test that if a UserSocialAuth object hasn't been attached to the pipeline as
|
||||
`social`, we return none
|
||||
"""
|
||||
request = mock.MagicMock(
|
||||
session={
|
||||
'running_pipeline': {
|
||||
'kwargs': {}
|
||||
}
|
||||
}
|
||||
)
|
||||
real_social = pipeline.get_real_social_auth_object(request)
|
||||
self.assertEqual(real_social, None)
|
||||
|
||||
def test_quarantine(self):
|
||||
"""
|
||||
Test that quarantining a session adds the correct flags, and that
|
||||
lifting the quarantine similarly removes those flags.
|
||||
"""
|
||||
request = mock.MagicMock(
|
||||
session={}
|
||||
)
|
||||
pipeline.quarantine_session(request, locations=('my_totally_real_module', 'other_real_module',))
|
||||
self.assertEqual(
|
||||
request.session['third_party_auth_quarantined_modules'],
|
||||
('my_totally_real_module', 'other_real_module',),
|
||||
)
|
||||
pipeline.lift_quarantine(request)
|
||||
self.assertNotIn('third_party_auth_quarantined_modules', request.session)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from third_party_auth import provider, settings
|
||||
from third_party_auth.tests import testutil
|
||||
from util.enterprise_helpers import enterprise_enabled
|
||||
import unittest
|
||||
|
||||
|
||||
@@ -55,3 +56,9 @@ class SettingsUnitTest(testutil.TestCase):
|
||||
# bad in prod.
|
||||
settings.apply_settings(self.settings)
|
||||
self.assertFalse(self.settings.SOCIAL_AUTH_RAISE_EXCEPTIONS)
|
||||
|
||||
@unittest.skipUnless(enterprise_enabled(), 'enterprise not enabled')
|
||||
def test_enterprise_elements_inserted(self):
|
||||
settings.apply_settings(self.settings)
|
||||
self.assertIn('enterprise.tpa_pipeline.set_data_sharing_consent_record', self.settings.SOCIAL_AUTH_PIPELINE)
|
||||
self.assertIn('enterprise.tpa_pipeline.verify_data_sharing_consent', self.settings.SOCIAL_AUTH_PIPELINE)
|
||||
|
||||
122
common/djangoapps/util/enterprise_helpers.py
Normal file
122
common/djangoapps/util/enterprise_helpers.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Helpers to access the enterprise app
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
try:
|
||||
from enterprise.models import EnterpriseCustomer
|
||||
from enterprise.tpa_pipeline import (
|
||||
active_provider_requests_data_sharing,
|
||||
active_provider_enforces_data_sharing,
|
||||
get_enterprise_customer_for_request,
|
||||
)
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
|
||||
|
||||
def enterprise_enabled():
|
||||
"""
|
||||
Determines whether the Enterprise app is installed
|
||||
"""
|
||||
return 'enterprise' in settings.INSTALLED_APPS
|
||||
|
||||
|
||||
def data_sharing_consent_requested(request):
|
||||
"""
|
||||
Determine if the EnterpriseCustomer for a given HTTP request
|
||||
requests data sharing consent
|
||||
"""
|
||||
if not enterprise_enabled():
|
||||
return False
|
||||
return active_provider_requests_data_sharing(request)
|
||||
|
||||
|
||||
def data_sharing_consent_required_at_login(request):
|
||||
"""
|
||||
Determines if data sharing consent is required at
|
||||
a given location
|
||||
"""
|
||||
if not enterprise_enabled():
|
||||
return False
|
||||
return active_provider_enforces_data_sharing(request, EnterpriseCustomer.AT_LOGIN)
|
||||
|
||||
|
||||
def data_sharing_consent_requirement_at_login(request):
|
||||
"""
|
||||
Returns either 'optional' or 'required' based on where we are.
|
||||
"""
|
||||
if not enterprise_enabled():
|
||||
return None
|
||||
if data_sharing_consent_required_at_login(request):
|
||||
return 'required'
|
||||
if data_sharing_consent_requested(request):
|
||||
return 'optional'
|
||||
return None
|
||||
|
||||
|
||||
def insert_enterprise_fields(request, form_desc):
|
||||
"""
|
||||
Enterprise methods which modify the logistration form are called from this method.
|
||||
"""
|
||||
if not enterprise_enabled():
|
||||
return
|
||||
add_data_sharing_consent_field(request, form_desc)
|
||||
|
||||
|
||||
def add_data_sharing_consent_field(request, form_desc):
|
||||
"""
|
||||
Adds a checkbox field to be selected if the user consents to share data with
|
||||
the EnterpriseCustomer attached to the SSO provider with which they're authenticating.
|
||||
"""
|
||||
enterprise_customer = get_enterprise_customer_for_request(request)
|
||||
required = data_sharing_consent_required_at_login(request)
|
||||
|
||||
if not data_sharing_consent_requested(request):
|
||||
return
|
||||
|
||||
label = _(
|
||||
"I agree to allow {platform_name} to share data about my enrollment, "
|
||||
"completion and performance in all {platform_name} courses and programs "
|
||||
"where my enrollment is sponsored by {ec_name}."
|
||||
).format(
|
||||
platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME),
|
||||
ec_name=enterprise_customer.name
|
||||
)
|
||||
|
||||
error_msg = _(
|
||||
"To link your account with {ec_name}, you are required to consent to data sharing."
|
||||
).format(
|
||||
ec_name=enterprise_customer.name
|
||||
)
|
||||
|
||||
form_desc.add_field(
|
||||
"data_sharing_consent",
|
||||
label=label,
|
||||
field_type="checkbox",
|
||||
default=False,
|
||||
required=required,
|
||||
error_messages={"required": error_msg},
|
||||
)
|
||||
|
||||
|
||||
def insert_enterprise_pipeline_elements(pipeline):
|
||||
"""
|
||||
If the enterprise app is enabled, insert additional elements into the
|
||||
pipeline so that data sharing consent views are used.
|
||||
"""
|
||||
if not enterprise_enabled():
|
||||
return
|
||||
|
||||
additional_elements = (
|
||||
'enterprise.tpa_pipeline.set_data_sharing_consent_record',
|
||||
'enterprise.tpa_pipeline.verify_data_sharing_consent',
|
||||
)
|
||||
# Find the item we need to insert the data sharing consent elements before
|
||||
insert_point = pipeline.index('social.pipeline.social_auth.load_extra_data')
|
||||
|
||||
for index, element in enumerate(additional_elements):
|
||||
pipeline.insert(insert_point + index, element)
|
||||
146
common/djangoapps/util/tests/test_enterprise_helpers.py
Normal file
146
common/djangoapps/util/tests/test_enterprise_helpers.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Test the enterprise app helpers
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
import mock
|
||||
|
||||
from util.enterprise_helpers import (
|
||||
enterprise_enabled,
|
||||
data_sharing_consent_requested,
|
||||
data_sharing_consent_required_at_login,
|
||||
data_sharing_consent_requirement_at_login,
|
||||
insert_enterprise_fields,
|
||||
insert_enterprise_pipeline_elements
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestEnterpriseHelpers(unittest.TestCase):
|
||||
"""
|
||||
Test enterprise app helpers
|
||||
"""
|
||||
|
||||
@mock.patch('util.enterprise_helpers.enterprise_enabled')
|
||||
def test_utils_with_enterprise_disabled(self, mock_enterprise_enabled):
|
||||
"""
|
||||
Test that the enterprise app not being available causes
|
||||
the utilities to return the expected default values.
|
||||
"""
|
||||
mock_enterprise_enabled.return_value = False
|
||||
self.assertFalse(data_sharing_consent_requested(None))
|
||||
self.assertFalse(data_sharing_consent_required_at_login(None))
|
||||
self.assertEqual(data_sharing_consent_requirement_at_login(None), None)
|
||||
self.assertEqual(insert_enterprise_fields(None, None), None)
|
||||
self.assertEqual(insert_enterprise_pipeline_elements(None), None)
|
||||
|
||||
def test_enterprise_enabled(self):
|
||||
"""
|
||||
The test settings inherit from common, which have the enterprise
|
||||
app installed; therefore, it should appear installed here.
|
||||
"""
|
||||
self.assertTrue(enterprise_enabled())
|
||||
|
||||
@mock.patch('enterprise.tpa_pipeline.get_enterprise_customer_for_request')
|
||||
def test_data_sharing_consent_requested(self, mock_get_ec):
|
||||
"""
|
||||
Test that we correctly check whether data sharing consent is requested.
|
||||
"""
|
||||
request = mock.MagicMock(session={'partial_pipeline': 'thing'})
|
||||
mock_get_ec.return_value = mock.MagicMock(requests_data_sharing_consent=True)
|
||||
self.assertTrue(data_sharing_consent_requested(request))
|
||||
mock_get_ec.return_value = mock.MagicMock(requests_data_sharing_consent=False)
|
||||
self.assertFalse(data_sharing_consent_requested(request))
|
||||
mock_get_ec.return_value = None
|
||||
self.assertFalse(data_sharing_consent_requested(request))
|
||||
request = mock.MagicMock(session={})
|
||||
self.assertFalse(data_sharing_consent_requested(request))
|
||||
|
||||
@mock.patch('enterprise.tpa_pipeline.get_enterprise_customer_for_request')
|
||||
def test_data_sharing_consent_required(self, mock_get_ec):
|
||||
"""
|
||||
Test that we correctly check whether data sharing consent is required at login.
|
||||
"""
|
||||
check_method = mock.MagicMock(return_value=True)
|
||||
request = mock.MagicMock(session={'partial_pipeline': 'thing'})
|
||||
mock_get_ec.return_value = mock.MagicMock(enforces_data_sharing_consent=check_method)
|
||||
self.assertTrue(data_sharing_consent_required_at_login(request))
|
||||
check_method.return_value = False
|
||||
mock_get_ec.return_value = mock.MagicMock(enforces_data_sharing_consent=check_method)
|
||||
self.assertFalse(data_sharing_consent_required_at_login(request))
|
||||
mock_get_ec.return_value = None
|
||||
self.assertFalse(data_sharing_consent_required_at_login(request))
|
||||
request = mock.MagicMock(session={})
|
||||
self.assertFalse(data_sharing_consent_required_at_login(request))
|
||||
|
||||
@mock.patch('enterprise.tpa_pipeline.get_enterprise_customer_for_request')
|
||||
def test_data_sharing_consent_requirement(self, mock_get_ec):
|
||||
"""
|
||||
Test that we get the correct requirement string for the current consent statae.
|
||||
"""
|
||||
request = mock.MagicMock(session={'partial_pipeline': 'thing'})
|
||||
mock_ec = mock.MagicMock(
|
||||
enforces_data_sharing_consent=mock.MagicMock(return_value=True),
|
||||
requests_data_sharing_consent=True,
|
||||
)
|
||||
mock_get_ec.return_value = mock_ec
|
||||
self.assertEqual(data_sharing_consent_requirement_at_login(request), 'required')
|
||||
mock_ec.enforces_data_sharing_consent.return_value = False
|
||||
self.assertEqual(data_sharing_consent_requirement_at_login(request), 'optional')
|
||||
mock_ec.requests_data_sharing_consent = False
|
||||
self.assertEqual(data_sharing_consent_requirement_at_login(request), None)
|
||||
|
||||
@mock.patch('util.enterprise_helpers.get_enterprise_customer_for_request')
|
||||
@mock.patch('enterprise.tpa_pipeline.get_enterprise_customer_for_request')
|
||||
@mock.patch('util.enterprise_helpers.configuration_helpers')
|
||||
def test_insert_enterprise_fields(self, mock_config_helpers, mock_get_ec, mock_get_ec2):
|
||||
"""
|
||||
Test that the insertion of the enterprise fields is processed as expected.
|
||||
"""
|
||||
request = mock.MagicMock(session={'partial_pipeline': 'thing'})
|
||||
mock_ec = mock.MagicMock(
|
||||
enforces_data_sharing_consent=mock.MagicMock(return_value=True),
|
||||
requests_data_sharing_consent=True,
|
||||
)
|
||||
# Name values in a MagicMock constructor don't fill a `name` attribute
|
||||
mock_ec.name = 'MassiveCorp'
|
||||
mock_get_ec.return_value = mock_ec
|
||||
mock_get_ec2.return_value = mock_ec
|
||||
mock_config_helpers.get_value.return_value = 'OpenEdX'
|
||||
form_desc = mock.MagicMock()
|
||||
form_desc.add_field.return_value = None
|
||||
expected_label = (
|
||||
"I agree to allow OpenEdX to share data about my enrollment, "
|
||||
"completion and performance in all OpenEdX courses and programs "
|
||||
"where my enrollment is sponsored by MassiveCorp."
|
||||
)
|
||||
expected_err_msg = (
|
||||
"To link your account with MassiveCorp, you are required to consent to data sharing."
|
||||
)
|
||||
insert_enterprise_fields(request, form_desc)
|
||||
mock_ec.enforces_data_sharing_consent.return_value = False
|
||||
insert_enterprise_fields(request, form_desc)
|
||||
calls = [
|
||||
mock.call(
|
||||
'data_sharing_consent',
|
||||
label=expected_label,
|
||||
field_type='checkbox',
|
||||
default=False,
|
||||
required=True,
|
||||
error_messages={'required': expected_err_msg}
|
||||
),
|
||||
mock.call(
|
||||
'data_sharing_consent',
|
||||
label=expected_label,
|
||||
field_type='checkbox',
|
||||
default=False,
|
||||
required=False,
|
||||
error_messages={'required': expected_err_msg}
|
||||
)
|
||||
]
|
||||
form_desc.add_field.assert_has_calls(calls)
|
||||
form_desc.add_field.reset_mock()
|
||||
mock_ec.requests_data_sharing_consent = False
|
||||
insert_enterprise_fields(request, form_desc)
|
||||
form_desc.add_field.assert_not_called()
|
||||
@@ -2662,7 +2662,6 @@ OPTIONAL_APPS = (
|
||||
|
||||
# Enterprise App (http://github.com/edx/edx-enterprise)
|
||||
'enterprise',
|
||||
|
||||
# Required by the Enterprise App
|
||||
'django_object_actions', # https://github.com/crccheck/django-object-actions
|
||||
)
|
||||
|
||||
@@ -245,6 +245,7 @@
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&[for="register-data_sharing_consent"],
|
||||
&[for="register-honor_code"],
|
||||
&[for="register-terms_of_service"] {
|
||||
display: inline-block;
|
||||
|
||||
@@ -17,6 +17,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from django_comment_common.models import ForumsConfig
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from util.enterprise_helpers import enterprise_enabled
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
|
||||
@@ -859,6 +860,12 @@ if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
|
||||
url(r'^login_oauth_token/(?P<backend>[^/]+)/$', 'student.views.login_oauth_token'),
|
||||
)
|
||||
|
||||
# Enterprise
|
||||
if enterprise_enabled():
|
||||
urlpatterns += (
|
||||
url(r'', include('enterprise.urls')),
|
||||
)
|
||||
|
||||
# OAuth token exchange
|
||||
if settings.FEATURES.get('ENABLE_OAUTH2_PROVIDER'):
|
||||
urlpatterns += (
|
||||
|
||||
@@ -1026,6 +1026,43 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
}
|
||||
)
|
||||
|
||||
@mock.patch('util.enterprise_helpers.active_provider_requests_data_sharing')
|
||||
@mock.patch('util.enterprise_helpers.active_provider_enforces_data_sharing')
|
||||
@mock.patch('util.enterprise_helpers.get_enterprise_customer_for_request')
|
||||
@mock.patch('util.enterprise_helpers.configuration_helpers')
|
||||
def test_register_form_consent_field(self, config_helper, get_ec, mock_enforce, mock_request):
|
||||
"""
|
||||
Test that if we have an EnterpriseCustomer active for the request, and that
|
||||
EnterpriseCustomer is set to require data sharing consent, the correct
|
||||
field is added to the form descriptor.
|
||||
"""
|
||||
fake_ec = mock.MagicMock(
|
||||
enforces_data_sharing_consent=mock.MagicMock(return_value=True),
|
||||
requests_data_sharing_consent=True,
|
||||
)
|
||||
fake_ec.name = 'MegaCorp'
|
||||
get_ec.return_value = fake_ec
|
||||
config_helper.get_value.return_value = 'OpenEdX'
|
||||
mock_request.return_value = True
|
||||
mock_enforce.return_value = True
|
||||
self._assert_reg_field(
|
||||
dict(),
|
||||
{
|
||||
u"name": u"data_sharing_consent",
|
||||
u"type": u"checkbox",
|
||||
u"required": True,
|
||||
u"label": (
|
||||
"I agree to allow OpenEdX to share data about my enrollment, "
|
||||
"completion and performance in all OpenEdX courses and programs "
|
||||
"where my enrollment is sponsored by MegaCorp."
|
||||
),
|
||||
u"defaultValue": False,
|
||||
u"errorMessages": {
|
||||
u'required': u'To link your account with MegaCorp, you are required to consent to data sharing.',
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@mock.patch('openedx.core.djangoapps.user_api.views._')
|
||||
def test_register_form_level_of_education_translations(self, fake_gettext):
|
||||
fake_gettext.side_effect = lambda text: text + ' TRANSLATED'
|
||||
|
||||
@@ -31,6 +31,7 @@ from student.cookies import set_logged_in_cookies
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.lib.api.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from util.json_request import JsonResponse
|
||||
from util.enterprise_helpers import insert_enterprise_fields
|
||||
from .preferences.api import get_country_time_zones, update_email_opt_in
|
||||
from .helpers import FormDescription, shim_student_view, require_post_params
|
||||
from .models import UserPreference, UserProfile
|
||||
@@ -279,6 +280,9 @@ class RegistrationView(APIView):
|
||||
required=self._is_field_required(field_name)
|
||||
)
|
||||
|
||||
# Add any Enterprise fields if the app is enabled
|
||||
insert_enterprise_fields(request, form_desc)
|
||||
|
||||
return HttpResponse(form_desc.to_json(), content_type="application/json")
|
||||
|
||||
@method_decorator(csrf_exempt)
|
||||
|
||||
@@ -46,7 +46,7 @@ edx-drf-extensions==1.2.1
|
||||
edx-lint==0.4.3
|
||||
edx-django-oauth2-provider==1.1.4
|
||||
edx-django-sites-extensions==2.1.1
|
||||
edx-enterprise==0.1.0
|
||||
edx-enterprise==0.6.0
|
||||
edx-oauth2-provider==1.2.0
|
||||
edx-opaque-keys==0.4.0
|
||||
edx-organizations==0.4.1
|
||||
|
||||
Reference in New Issue
Block a user