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
7ee7093b
Commit
7ee7093b
authored
Aug 31, 2015
by
E. Kolpakov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Automatic account provisioning for opted-in SAML providers
parent
9d4b82f3
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
479 additions
and
21 deletions
+479
-21
common/djangoapps/student/views.py
+23
-9
common/djangoapps/third_party_auth/migrations/0004_auto__add_field_samlproviderconfig_autoprovision_account__add_field_oa.py
+129
-0
common/djangoapps/third_party_auth/models.py
+9
-0
common/djangoapps/third_party_auth/pipeline.py
+52
-8
common/djangoapps/third_party_auth/settings.py
+10
-2
common/djangoapps/third_party_auth/strategy.py
+45
-0
common/djangoapps/third_party_auth/tests/specs/test_testshib.py
+66
-2
common/djangoapps/third_party_auth/tests/test_pipeline.py
+37
-0
common/djangoapps/third_party_auth/tests/test_strategy.py
+97
-0
lms/envs/aws.py
+8
-0
lms/envs/test.py
+3
-0
No files found.
common/djangoapps/student/views.py
View file @
7ee7093b
...
...
@@ -1357,11 +1357,22 @@ def change_setting(request):
class
AccountValidationError
(
Exception
):
""" Exception thrown if some account validation error happened """
def
__init__
(
self
,
message
,
field
):
super
(
AccountValidationError
,
self
)
.
__init__
(
message
)
self
.
field
=
field
class
AccountUserNameValidationError
(
AccountValidationError
):
""" Exception thrown if attempted to create account with username already taken """
pass
class
AccountEmailAlreadyExistsValidationError
(
AccountValidationError
):
""" Exception thrown if attempted to create account with email already used by other account """
pass
@receiver
(
post_save
,
sender
=
User
)
def
user_signup_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
...
...
@@ -1403,12 +1414,12 @@ def _do_create_account(form):
except
IntegrityError
:
# Figure out the cause of the integrity error
if
len
(
User
.
objects
.
filter
(
username
=
user
.
username
))
>
0
:
raise
AccountValidationError
(
raise
Account
UserName
ValidationError
(
_
(
"An account with the Public Username '{username}' already exists."
)
.
format
(
username
=
user
.
username
),
field
=
"username"
)
elif
len
(
User
.
objects
.
filter
(
email
=
user
.
email
))
>
0
:
raise
AccountValidationError
(
raise
Account
EmailAlreadyExists
ValidationError
(
_
(
"An account with the Email '{email}' already exists."
)
.
format
(
email
=
user
.
email
),
field
=
"email"
)
...
...
@@ -1442,7 +1453,8 @@ def _do_create_account(form):
return
(
user
,
profile
,
registration
)
def
create_account_with_params
(
request
,
params
):
# pylint: disable=too-many-statements
def
create_account_with_params
(
request
,
params
,
skip_email
=
False
):
"""
Given a request and a dict of parameters (which may or may not have come
from the request), create an account for the requesting user, including
...
...
@@ -1536,7 +1548,8 @@ def create_account_with_params(request, params):
(
user
,
profile
,
registration
)
=
_do_create_account
(
form
)
# next, link the account with social auth, if provided via the API.
# (If the user is using the normal register page, the social auth pipeline does the linking, not this code)
# (If the user is using the normal register page or account is automatically provisioned,
# the social auth pipeline does the linking, not this code)
if
should_link_with_social_auth
:
backend_name
=
params
[
'provider'
]
request
.
social_strategy
=
social_utils
.
load_strategy
(
request
)
...
...
@@ -1620,11 +1633,12 @@ def create_account_with_params(request, params):
# the other for *new* systems. we need to be careful about
# changing settings on a running system to make sure no users are
# left in an inconsistent state (or doing a migration if they are).
send_email
=
(
not
settings
.
FEATURES
.
get
(
'SKIP_EMAIL_VALIDATION'
,
None
)
and
not
settings
.
FEATURES
.
get
(
'AUTOMATIC_AUTH_FOR_TESTING'
)
and
not
(
do_external_auth
and
settings
.
FEATURES
.
get
(
'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'
))
and
not
(
send_email
=
not
(
skip_email
or
settings
.
FEATURES
.
get
(
'SKIP_EMAIL_VALIDATION'
,
False
)
or
settings
.
FEATURES
.
get
(
'AUTOMATIC_AUTH_FOR_TESTING'
,
False
)
or
(
do_external_auth
and
settings
.
FEATURES
.
get
(
'BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'
))
or
(
third_party_provider
and
third_party_provider
.
skip_email_verification
and
user
.
email
==
running_pipeline
[
'kwargs'
]
.
get
(
'details'
,
{})
.
get
(
'email'
)
)
...
...
common/djangoapps/third_party_auth/migrations/0004_auto__add_field_samlproviderconfig_autoprovision_account__add_field_oa.py
0 → 100644
View file @
7ee7093b
This diff is collapsed.
Click to expand it.
common/djangoapps/third_party_auth/models.py
View file @
7ee7093b
...
...
@@ -93,6 +93,15 @@ class ProviderConfig(ConfigurationModel):
"email, and their account will be activated immediately upon registration."
),
)
autoprovision_account
=
models
.
BooleanField
(
default
=
False
,
help_text
=
_
(
"If this option is selected, users will not be required to confirm their details even if "
"some required data is missing or fails validation (e.g. duplicate email). Instead, fake or generated "
"values will be used. This setting forces skipping email verification, so 'Skip email verification' "
"setting have no effect."
)
)
prefix
=
None
# used for provider_id. Set to a string value in subclass
backend_name
=
None
# Set to a field or fixed value in subclass
...
...
common/djangoapps/third_party_auth/pipeline.py
View file @
7ee7093b
...
...
@@ -56,12 +56,12 @@ rather than spreading them across two functions in the pipeline.
See http://psa.matiasaguirre.net/docs/pipeline.html for more docs.
"""
import
random
import
string
# pylint: disable-msg=deprecated-module
from
collections
import
OrderedDict
import
urllib
import
analytics
from
django.conf
import
settings
from
eventtracking
import
tracker
from
django.contrib.auth.models
import
User
...
...
@@ -73,6 +73,7 @@ from social.apps.django_app.default import models
from
social.exceptions
import
AuthException
from
social.pipeline
import
partial
from
social.pipeline.social_auth
import
associate_by_email
from
social.pipeline.user
import
create_user
as
social_create_user
import
student
...
...
@@ -80,9 +81,6 @@ from logging import getLogger
from
.
import
provider
# Note that this lives in openedx, so this dependency should be refactored.
from
openedx.core.djangoapps.user_api.preferences.api
import
update_email_opt_in
# These are the query string params you can pass
# to the URL that starts the authentication process.
...
...
@@ -189,6 +187,20 @@ class NotActivatedException(AuthException):
)
class
EmailAlreadyInUseException
(
AuthException
):
""" Raised when new user account is created with an email already used by another account """
def
__init__
(
self
,
backend
,
email
):
self
.
email
=
email
super
(
EmailAlreadyInUseException
,
self
)
.
__init__
(
backend
,
email
)
def
__str__
(
self
):
return
(
_
(
'Email {email_address} is already used in our system. To link your accounts, '
'sign in now using your {platform_name} password.'
)
.
format
(
email_address
=
self
.
email
,
platform_name
=
settings
.
PLATFORM_NAME
)
)
class
ProviderUserState
(
object
):
"""Object representing the provider state (attached or not) for a user.
...
...
@@ -503,18 +515,34 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
"""Redirects to the registration page."""
return
redirect
(
AUTH_DISPATCH_URLS
[
AUTH_ENTRY_REGISTER
])
def
should_force_account_creation
():
""" For some third party providers, we auto-create user accounts """
current_provider
=
provider
.
Registry
.
get_from_pipeline
({
'backend'
:
backend
.
name
,
'kwargs'
:
kwargs
})
def
get_provider
():
"""
Gets third-party provider for request
"""
return
provider
.
Registry
.
get_from_pipeline
({
'backend'
:
backend
.
name
,
'kwargs'
:
kwargs
})
def
should_autoprovision_account
():
""" For some third party providers we trust the provider so much that we automatically provision the account """
current_provider
=
get_provider
()
return
current_provider
and
current_provider
.
autoprovision_account
def
autosubmit_registration_form
():
""" For some third party providers, we auto-submit registration forms """
current_provider
=
get_provider
()
return
current_provider
and
current_provider
.
skip_email_verification
if
not
user
:
if
should_autoprovision_account
():
# User has authenticated with the third party provider and provider is configured
# to automatically provision edX account, which is done via strategy.create_user in next pipeline step
return
{
'autoprovision'
:
True
}
if
auth_entry
in
[
AUTH_ENTRY_LOGIN_API
,
AUTH_ENTRY_REGISTER_API
]:
return
HttpResponseBadRequest
()
elif
auth_entry
in
[
AUTH_ENTRY_LOGIN
,
AUTH_ENTRY_LOGIN_2
]:
# User has authenticated with the third party provider but we don't know which edX
# account corresponds to them yet, if any.
if
should_force_account_creation
():
if
autosubmit_registration_form
():
return
dispatch_to_register
()
return
dispatch_to_login
()
elif
auth_entry
in
[
AUTH_ENTRY_REGISTER
,
AUTH_ENTRY_REGISTER_2
]:
...
...
@@ -555,6 +583,22 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
@partial.partial
def
create_user
(
strategy
,
details
,
user
=
None
,
*
args
,
**
kwargs
):
"""
Substitution method for stock social create_user that catches email validation error and redirects to login
"""
from
student.views
import
AccountEmailAlreadyExistsValidationError
try
:
return
social_create_user
(
strategy
,
details
,
user
,
*
args
,
**
kwargs
)
except
AccountEmailAlreadyExistsValidationError
as
exc
:
logger
.
exception
(
exc
.
message
)
# We're raising an exception that inherits from AuthException. Such exceptions are properly handled
# by social auth pipeline: their string representation (see __str__ method) is displayed to user on the page
# we're redirecting to.
raise
EmailAlreadyInUseException
(
exc
.
message
,
details
[
'email'
])
@partial.partial
def
set_logged_in_cookies
(
backend
=
None
,
user
=
None
,
strategy
=
None
,
auth_entry
=
None
,
*
args
,
**
kwargs
):
"""This pipeline step sets the "logged in" cookie for authenticated users.
...
...
common/djangoapps/third_party_auth/settings.py
View file @
7ee7093b
...
...
@@ -9,7 +9,6 @@ If true, it:
a) loads this module.
b) calls apply_settings(), passing in the Django settings
"""
_FIELDS_STORED_IN_SESSION
=
[
'auth_entry'
,
'next'
]
_MIDDLEWARE_CLASSES
=
(
'third_party_auth.middleware.ExceptionMiddleware'
,
...
...
@@ -53,7 +52,7 @@ def apply_settings(django_settings):
'social.pipeline.user.get_username'
,
'third_party_auth.pipeline.set_pipeline_timeout'
,
'third_party_auth.pipeline.ensure_user_information'
,
'
social.pipeline.user
.create_user'
,
'
third_party_auth.pipeline
.create_user'
,
'social.pipeline.social_auth.associate_user'
,
'social.pipeline.social_auth.load_extra_data'
,
'social.pipeline.user.user_details'
,
...
...
@@ -85,3 +84,12 @@ def apply_settings(django_settings):
'social.apps.django_app.context_processors.backends'
,
'social.apps.django_app.context_processors.login_redirect'
,
)
# These fields are grabbed from third party auth response and passed to strategy.create_user
# If autoprovisioning an account we want as much data preserved as possible, so we try to get those as well
# If they are not available it would just pass None and should not crash, unless consuming code depends on those
# values being set, which is not the case by the time of writing
if
not
hasattr
(
django_settings
,
'SOCIAL_AUTH_USER_FIELDS'
):
django_settings
.
SOCIAL_AUTH_USER_FIELDS
=
getattr
(
django_settings
,
'USER_FIELDS'
,
[
'username'
,
'email'
,
'first_name'
,
'last_name'
,
'fullname'
]
)
common/djangoapps/third_party_auth/strategy.py
View file @
7ee7093b
...
...
@@ -2,11 +2,15 @@
A custom Strategy for python-social-auth that allows us to fetch configuration from
ConfigurationModels rather than django.settings
"""
import
logging
from
.models
import
OAuth2ProviderConfig
from
social.backends.oauth
import
BaseOAuth2
from
social.strategies.django_strategy
import
DjangoStrategy
log
=
logging
.
getLogger
(
__name__
)
class
ConfigurationModelStrategy
(
DjangoStrategy
):
"""
A DjangoStrategy customized to load settings from ConfigurationModels
...
...
@@ -32,3 +36,44 @@ class ConfigurationModelStrategy(DjangoStrategy):
# At this point, we know 'name' is not set in a [OAuth2|SAML]ProviderConfig row.
# It's probably a global Django setting like 'FIELDS_STORED_IN_SESSION':
return
super
(
ConfigurationModelStrategy
,
self
)
.
setting
(
name
,
default
,
backend
)
def
_ensure_passes_length_check
(
self
,
user_data
,
key
,
fallback
,
min_length
=
2
):
"""
Ensures that value we get from user_data is meets length requirements. IF it is shorter than required, fallback
is used
"""
assert
len
(
fallback
)
>=
min_length
value
=
user_data
.
get
(
key
)
if
value
and
len
(
value
)
>=
min_length
:
return
value
return
fallback
def
create_user
(
self
,
*
args
,
**
kwargs
):
"""
# Creates user using information provided by pipeline. This method is called in create_user pipeline step.
# Unless the workflow is changed, create_user immediately terminates if the user already found/
# So far, user is either created in ensure_user_information via registration form or account needs to be
# autoprovisioned. So, this method is only called when autoprovisioning account.
"""
from
student.views
import
create_account_with_params
from
.pipeline
import
make_random_password
user_fields
=
dict
(
kwargs
)
# needs to be >2 chars to pass validation
name
=
self
.
_ensure_passes_length_check
(
user_fields
,
'fullname'
,
self
.
setting
(
"THIRD_PARTY_AUTH_FALLBACK_FULL_NAME"
)
)
password
=
self
.
_ensure_passes_length_check
(
user_fields
,
'password'
,
make_random_password
())
user_fields
[
'name'
]
=
name
user_fields
[
'password'
]
=
password
user_fields
[
'honor_code'
]
=
True
user_fields
[
'terms_of_service'
]
=
True
if
not
user_fields
.
get
(
'email'
):
user_fields
[
'email'
]
=
"{username}@{domain}"
.
format
(
username
=
user_fields
[
'username'
],
domain
=
self
.
setting
(
"FAKE_EMAIL_DOMAIN"
)
)
# when autoprovisioning we need to skip email activation, hence skip_email is True
return
create_account_with_params
(
self
.
request
,
user_fields
,
skip_email
=
True
)
common/djangoapps/third_party_auth/tests/specs/test_testshib.py
View file @
7ee7093b
...
...
@@ -28,6 +28,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
super
(
TestShibIntegrationTest
,
self
)
.
setUp
()
self
.
login_page_url
=
reverse
(
'signin_user'
)
self
.
register_page_url
=
reverse
(
'register_user'
)
self
.
dashboard_page_url
=
reverse
(
'dashboard'
)
self
.
enable_saml
(
private_key
=
self
.
_get_private_key
(),
public_key
=
self
.
_get_public_key
(),
...
...
@@ -142,7 +143,56 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
continue_response
=
self
.
client
.
get
(
TPA_TESTSHIB_COMPLETE_URL
)
# And we should be redirected to the dashboard:
self
.
assertEqual
(
continue_response
.
status_code
,
302
)
self
.
assertEqual
(
continue_response
[
'Location'
],
self
.
url_prefix
+
reverse
(
'dashboard'
))
self
.
assertEqual
(
continue_response
[
'Location'
],
self
.
url_prefix
+
self
.
dashboard_page_url
)
# Now check that we can login again:
self
.
client
.
logout
()
self
.
_test_return_login
()
def
test_autoprovision_from_login
(
self
):
self
.
_configure_testshib_provider
(
autoprovision_account
=
True
)
self
.
_freeze_time
(
timestamp
=
1434326820
)
# This is the time when the saved request/response was recorded.
# check that we don't have a user we're autoprovisioning account for
self
.
_assert_user_does_not_exist
(
'myself'
)
# The user goes to the register page, and sees a button to register with TestShib:
self
.
_check_login_page
()
self
.
_test_autoprovision
(
TPA_TESTSHIB_LOGIN_URL
)
def
test_autoprovision_from_register
(
self
):
self
.
_configure_testshib_provider
(
autoprovision_account
=
True
)
self
.
_freeze_time
(
timestamp
=
1434326820
)
# This is the time when the saved request/response was recorded.
# check that we don't have a user we're autoprovisioning account for
self
.
_assert_user_does_not_exist
(
'myself'
)
# The user goes to the register page, and sees a button to register with TestShib:
self
.
_check_register_page
()
self
.
_test_autoprovision
(
TPA_TESTSHIB_REGISTER_URL
)
def
_test_autoprovision
(
self
,
entry_point
):
""" Actual autoprovision code """
# The user clicks on the TestShib button:
try_entry_response
=
self
.
client
.
get
(
entry_point
)
# The user should be redirected to TestShib:
self
.
assertEqual
(
try_entry_response
.
status_code
,
302
)
self
.
assertTrue
(
try_entry_response
[
'Location'
]
.
startswith
(
TESTSHIB_SSO_URL
))
# Now the user will authenticate with the SAML provider
self
.
_fake_testshib_login_and_return
()
# Then there's one more redirect to set logged_in cookie
continue_response
=
self
.
client
.
get
(
TPA_TESTSHIB_COMPLETE_URL
)
# We should be redirected to the dashboard screen since profile should be created and logged in
self
.
assertEqual
(
continue_response
.
status_code
,
302
)
self
.
assertEqual
(
continue_response
[
'Location'
],
self
.
url_prefix
+
self
.
dashboard_page_url
)
# assert account is created and activated
self
.
_assert_account_created
(
username
=
'myself'
,
email
=
'myself@testshib.org'
,
full_name
=
'Me Myself And I'
)
# Now check that we can login again:
self
.
client
.
logout
()
...
...
@@ -168,7 +218,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
# And then we should be redirected to the dashboard:
login_response
=
self
.
client
.
get
(
TPA_TESTSHIB_COMPLETE_URL
)
self
.
assertEqual
(
login_response
.
status_code
,
302
)
self
.
assertEqual
(
login_response
[
'Location'
],
self
.
url_prefix
+
reverse
(
'dashboard'
)
)
self
.
assertEqual
(
login_response
[
'Location'
],
self
.
url_prefix
+
self
.
dashboard_page_url
)
# Now we are logged in:
dashboard_response
=
self
.
client
.
get
(
reverse
(
'dashboard'
))
self
.
assertEqual
(
dashboard_response
.
status_code
,
200
)
...
...
@@ -205,6 +255,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
kwargs
.
setdefault
(
'metadata_source'
,
TESTSHIB_METADATA_URL
)
kwargs
.
setdefault
(
'icon_class'
,
'fa-university'
)
kwargs
.
setdefault
(
'attr_email'
,
'urn:oid:1.3.6.1.4.1.5923.1.1.1.6'
)
# eduPersonPrincipalName
kwargs
.
setdefault
(
'autoprovision_account'
,
False
)
self
.
configure_saml_provider
(
**
kwargs
)
if
fetch_metadata
:
...
...
@@ -228,3 +279,16 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
user
=
User
.
objects
.
get
(
email
=
email
)
user
.
is_active
=
True
user
.
save
()
def
_assert_user_does_not_exist
(
self
,
username
):
""" Asserts that user with specified username does not exist """
with
self
.
assertRaises
(
User
.
DoesNotExist
):
User
.
objects
.
get
(
username
=
username
)
def
_assert_account_created
(
self
,
username
,
email
,
full_name
):
""" Asserts that user with specified username exists, activated and have specified full name and email """
user
=
User
.
objects
.
get
(
username
=
username
)
self
.
assertIsNotNone
(
user
.
profile
)
self
.
assertEqual
(
user
.
email
,
email
)
self
.
assertEqual
(
user
.
profile
.
name
,
full_name
)
self
.
assertTrue
(
user
.
is_active
)
common/djangoapps/third_party_auth/tests/test_pipeline.py
View file @
7ee7093b
"""Unit tests for third_party_auth/pipeline.py."""
import
random
import
mock
from
student.views
import
AccountEmailAlreadyExistsValidationError
from
third_party_auth
import
pipeline
,
provider
from
third_party_auth.tests
import
testutil
import
unittest
...
...
@@ -43,3 +45,38 @@ class ProviderUserStateTestCase(testutil.TestCase):
google_provider
=
self
.
configure_google_provider
(
enabled
=
True
)
state
=
pipeline
.
ProviderUserState
(
google_provider
,
object
(),
1000
)
self
.
assertEqual
(
google_provider
.
provider_id
+
'_unlink_form'
,
state
.
get_unlink_form_name
())
@unittest.skipUnless
(
testutil
.
AUTH_FEATURE_ENABLED
,
'third_party_auth not enabled'
)
class
TestCreateUser
(
testutil
.
TestCase
):
"""
Tests for custom create_user step
"""
def
_raise_email_in_use_exception
(
self
,
*
unused_args
,
**
unused_kwargs
):
""" Helper to raise AccountEmailAlreadyExistsValidationError """
raise
AccountEmailAlreadyExistsValidationError
(
mock
.
Mock
(),
mock
.
Mock
())
def
test_create_user_normal_scenario
(
self
):
""" Tests happy path - user is created and results are returned intact """
retval
=
mock
.
Mock
()
with
mock
.
patch
(
"third_party_auth.pipeline.social_create_user"
)
as
patched_social_create_user
:
patched_social_create_user
.
return_value
=
retval
strategy
,
details
,
user
,
idx
=
mock
.
Mock
(),
{
'email'
:
'qwe@asd.com'
},
mock
.
Mock
(),
1
# pylint: disable=redundant-keyword-arg
result
=
pipeline
.
create_user
(
strategy
,
idx
,
details
=
details
,
user
=
user
)
self
.
assertEqual
(
result
,
retval
)
def
test_create_user_exception_scenario
(
self
):
"""
Tests sad path - expected exception is thrown, captured and transformed into AuthException subclass instance
"""
with
mock
.
patch
(
"third_party_auth.pipeline.social_create_user"
)
as
patched_social_create_user
:
patched_social_create_user
.
side_effect
=
self
.
_raise_email_in_use_exception
strategy
,
details
,
user
=
mock
.
Mock
(),
{
'email'
:
'qwe@asd.com'
},
mock
.
Mock
()
with
self
.
assertRaises
(
pipeline
.
EmailAlreadyInUseException
):
# pylint: disable=redundant-keyword-arg
pipeline
.
create_user
(
strategy
,
1
,
details
=
details
,
user
=
user
)
common/djangoapps/third_party_auth/tests/test_strategy.py
0 → 100644
View file @
7ee7093b
""" unittests for strategy.py """
import
unittest
import
ddt
import
mock
from
unittest
import
TestCase
from
third_party_auth.strategy
import
ConfigurationModelStrategy
from
third_party_auth.tests
import
testutil
@ddt.ddt
@mock.patch
(
'student.views.create_account_with_params'
)
@unittest.skipUnless
(
testutil
.
AUTH_FEATURE_ENABLED
,
'third_party_auth not enabled'
)
class
TestStrategy
(
TestCase
):
""" Unit tests for authentication strategy """
def
setUp
(
self
):
super
(
TestStrategy
,
self
)
.
setUp
()
self
.
request_mock
=
mock
.
Mock
()
self
.
strategy
=
ConfigurationModelStrategy
(
mock
.
Mock
(),
request
=
self
.
request_mock
)
def
_get_last_call_args
(
self
,
patched_create_account
):
""" Helper to get last call arguments from a mock """
args
,
unused_kwargs
=
patched_create_account
.
call_args
return
args
def
test_create_user_sets_tos_and_honor_code
(
self
,
patched_create_account
):
self
.
strategy
.
create_user
(
username
=
'myself'
)
self
.
assertTrue
(
patched_create_account
.
called
)
request
,
user_data
=
self
.
_get_last_call_args
(
patched_create_account
)
self
.
assertEqual
(
request
,
self
.
request_mock
)
self
.
assertTrue
(
user_data
[
'terms_of_service'
])
self
.
assertTrue
(
user_data
[
'honor_code'
])
@ddt.data
(
(
None
,
'Fallback Name'
,
'Fallback Name'
),
(
'q'
,
'Other Name'
,
'Other Name'
),
(
'q2'
,
'Other Name'
,
'q2'
),
(
'qwe'
,
'Other Name'
,
'qwe'
),
(
'user1'
,
'Fallback Name'
,
'user1'
)
)
@ddt.unpack
def
test_create_user_sets_name
(
self
,
full_name
,
fallback_name
,
expected_name
,
patched_create_account
):
with
mock
.
patch
.
object
(
self
.
strategy
,
'setting'
,
mock
.
Mock
())
as
patched_setting
:
patched_setting
.
return_value
=
fallback_name
self
.
strategy
.
create_user
(
username
=
'myself'
,
fullname
=
full_name
)
# it is actually always called, but this assertion is relaxed to allow not actually going to settings
# if there's no point in that
if
expected_name
==
fallback_name
:
self
.
assertIn
(
mock
.
call
(
"THIRD_PARTY_AUTH_FALLBACK_FULL_NAME"
),
patched_setting
.
mock_calls
)
_
,
user_data
=
self
.
_get_last_call_args
(
patched_create_account
)
self
.
assertEqual
(
user_data
[
'name'
],
expected_name
)
def
test_sets_password_if_missing
(
self
,
patched_create_account
):
self
.
strategy
.
create_user
(
username
=
'myself'
,
fullname
=
'myself'
)
_
,
user_data
=
self
.
_get_last_call_args
(
patched_create_account
)
self
.
assertIn
(
'password'
,
user_data
)
@ddt.data
(
(
None
,
False
),
(
'q'
,
False
),
(
'12'
,
False
),
(
'456'
,
True
),
(
'$up3r_$e(ur3_p/|$$w0rd'
,
True
),
)
@ddt.unpack
def
test_passes_password_if_specified
(
self
,
password
,
should_match
,
patched_create_account
):
self
.
strategy
.
create_user
(
username
=
'myself'
,
fullname
=
'myself'
,
password
=
password
)
_
,
user_data
=
self
.
_get_last_call_args
(
patched_create_account
)
self
.
assertIn
(
'password'
,
user_data
)
if
should_match
:
self
.
assertEqual
(
user_data
[
'password'
],
password
)
@ddt.data
(
(
None
,
'fallback_domain.com'
,
'myself@fallback_domain.com'
),
(
''
,
'other_domain.com'
,
'myself@other_domain.com'
),
(
'qwe@asd.com'
,
'fallback_domain.com'
,
'qwe@asd.com'
),
(
'zxc@darpa.gov.mil.edu'
,
'fallback_domain.com'
,
'zxc@darpa.gov.mil.edu'
),
)
@ddt.unpack
def
test_sets_email_if_not_provided
(
self
,
email
,
fallback_domain
,
expected_email
,
patched_create_account
):
with
mock
.
patch
.
object
(
self
.
strategy
,
'setting'
,
mock
.
Mock
())
as
patched_setting
:
patched_setting
.
return_value
=
fallback_domain
# fullname is needed to avoid calling setting twice
self
.
strategy
.
create_user
(
username
=
'myself'
,
fullname
=
'myself'
,
email
=
email
)
# it is actually always called, but this assertion is relaxed to allow not actually going to settings
# if there's no point in that
if
email
!=
expected_email
:
self
.
assertIn
(
mock
.
call
(
"FAKE_EMAIL_DOMAIN"
),
patched_setting
.
mock_calls
)
_
,
user_data
=
self
.
_get_last_call_args
(
patched_create_account
)
self
.
assertEqual
(
user_data
[
'email'
],
expected_email
)
lms/envs/aws.py
View file @
7ee7093b
...
...
@@ -590,6 +590,14 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
'schedule'
:
datetime
.
timedelta
(
hours
=
ENV_TOKENS
.
get
(
'THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS'
,
24
)),
}
# FAKE EMAIL DOMAIN setting is used to generate an email for an automatically provisioned account in case
# it is not provided by IdP (which should'nt normally be the case for providers with automatic provisioning)
FAKE_EMAIL_DOMAIN
=
ENV_TOKENS
.
get
(
'FAKE_EMAIL_DOMAIN'
,
'fake-email-domain.foo'
)
# This setting is used as a user full name when automatically provisioning an account in case
# IdP provided name is empty, missing or does not pass minimal length check
THIRD_PARTY_AUTH_FALLBACK_FULL_NAME
=
ENV_TOKENS
.
get
(
'THIRD_PARTY_AUTH_FALLBACK_FULL_NAME'
,
"Unknown"
)
##### OAUTH2 Provider ##############
if
FEATURES
.
get
(
'ENABLE_OAUTH2_PROVIDER'
):
OAUTH_OIDC_ISSUER
=
ENV_TOKENS
[
'OAUTH_OIDC_ISSUER'
]
...
...
lms/envs/test.py
View file @
7ee7093b
...
...
@@ -258,6 +258,9 @@ AUTHENTICATION_BACKENDS = (
'third_party_auth.saml.SAMLAuthBackend'
,
)
+
AUTHENTICATION_BACKENDS
FAKE_EMAIL_DOMAIN
=
'fake-email-domain.foo'
THIRD_PARTY_AUTH_FALLBACK_FULL_NAME
=
"Unknown"
################################## OPENID #####################################
FEATURES
[
'AUTH_USE_OPENID'
]
=
True
FEATURES
[
'AUTH_USE_OPENID_PROVIDER'
]
=
True
...
...
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