Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
fc814aee
Commit
fc814aee
authored
Dec 12, 2016
by
Jesse Shapiro
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add data sharing consent features for EnterpriseCustomer
parent
388ed865
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
574 additions
and
13 deletions
+574
-13
common/djangoapps/student/forms.py
+13
-0
common/djangoapps/student/views.py
+8
-0
common/djangoapps/third_party_auth/middleware.py
+31
-0
common/djangoapps/third_party_auth/pipeline.py
+33
-0
common/djangoapps/third_party_auth/settings.py
+8
-2
common/djangoapps/third_party_auth/tests/specs/base.py
+7
-6
common/djangoapps/third_party_auth/tests/specs/test_testshib.py
+5
-3
common/djangoapps/third_party_auth/tests/test_middleware.py
+51
-0
common/djangoapps/third_party_auth/tests/test_pipeline_integration.py
+93
-0
common/djangoapps/third_party_auth/tests/test_settings.py
+7
-0
common/djangoapps/util/enterprise_helpers.py
+122
-0
common/djangoapps/util/tests/test_enterprise_helpers.py
+146
-0
lms/envs/common.py
+0
-1
lms/static/sass/views/_login-register.scss
+1
-0
lms/urls.py
+7
-0
openedx/core/djangoapps/user_api/tests/test_views.py
+37
-0
openedx/core/djangoapps/user_api/views.py
+4
-0
requirements/edx/base.txt
+1
-1
No files found.
common/djangoapps/student/forms.py
View file @
fc814aee
...
...
@@ -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
...
...
common/djangoapps/student/views.py
View file @
fc814aee
...
...
@@ -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
:
...
...
common/djangoapps/third_party_auth/middleware.py
View file @
fc814aee
...
...
@@ -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
()
common/djangoapps/third_party_auth/pipeline.py
View file @
fc814aee
...
...
@@ -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.
...
...
common/djangoapps/third_party_auth/settings.py
View file @
fc814aee
...
...
@@ -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'
...
...
common/djangoapps/third_party_auth/tests/specs/base.py
View file @
fc814aee
...
...
@@ -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:
...
...
common/djangoapps/third_party_auth/tests/specs/test_testshib.py
View file @
fc814aee
...
...
@@ -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 """
...
...
common/djangoapps/third_party_auth/tests/test_middleware.py
0 → 100644
View file @
fc814aee
"""
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
)
common/djangoapps/third_party_auth/tests/test_pipeline_integration.py
View file @
fc814aee
...
...
@@ -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
)
common/djangoapps/third_party_auth/tests/test_settings.py
View file @
fc814aee
...
...
@@ -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
)
common/djangoapps/util/enterprise_helpers.py
0 → 100644
View file @
fc814aee
"""
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
)
common/djangoapps/util/tests/test_enterprise_helpers.py
0 → 100644
View file @
fc814aee
"""
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
()
lms/envs/common.py
View file @
fc814aee
...
...
@@ -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
)
...
...
lms/static/sass/views/_login-register.scss
View file @
fc814aee
...
...
@@ -245,6 +245,7 @@
color
:
$red
;
}
&
[
for
=
"register-data_sharing_consent"
],
&
[
for
=
"register-honor_code"
],
&
[
for
=
"register-terms_of_service"
]
{
display
:
inline-block
;
...
...
lms/urls.py
View file @
fc814aee
...
...
@@ -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
+=
(
...
...
openedx/core/djangoapps/user_api/tests/test_views.py
View file @
fc814aee
...
...
@@ -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'
...
...
openedx/core/djangoapps/user_api/views.py
View file @
fc814aee
...
...
@@ -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
)
...
...
requirements/edx/base.txt
View file @
fc814aee
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment