Commit 5ece5fa0 by Eugeny Kolpakov

Merge pull request #546 from edx-solutions/sso/autoprovision_revert

Revert "Automatic account provisioning for opted-in SAML providers"
parents 00d2c9c9 3433906b
......@@ -1357,22 +1357,11 @@ 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
"""
......@@ -1414,12 +1403,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 AccountUserNameValidationError(
raise AccountValidationError(
_("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 AccountEmailAlreadyExistsValidationError(
raise AccountValidationError(
_("An account with the Email '{email}' already exists.").format(email=user.email),
field="email"
)
......@@ -1454,7 +1443,7 @@ def _do_create_account(form):
# pylint: disable=too-many-statements
def create_account_with_params(request, params, skip_email=False):
def create_account_with_params(request, params):
"""
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
......@@ -1548,8 +1537,7 @@ def create_account_with_params(request, params, skip_email=False):
(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 or account is automatically provisioned,
# the social auth pipeline does the linking, not this code)
# (If the user is using the normal register page, 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)
......@@ -1633,12 +1621,11 @@ def create_account_with_params(request, params, skip_email=False):
# 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 (
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
(
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 (
third_party_provider and third_party_provider.skip_email_verification and
user.email == running_pipeline['kwargs'].get('details', {}).get('email')
)
......
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'SAMLProviderConfig.autoprovision_account'
db.delete_column('third_party_auth_samlproviderconfig', 'autoprovision_account')
# Deleting field 'OAuth2ProviderConfig.autoprovision_account'
db.delete_column('third_party_auth_oauth2providerconfig', 'autoprovision_account')
def backwards(self, orm):
# Adding field 'SAMLProviderConfig.autoprovision_account'
db.add_column('third_party_auth_samlproviderconfig', 'autoprovision_account',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'OAuth2ProviderConfig.autoprovision_account'
db.add_column('third_party_auth_oauth2providerconfig', 'autoprovision_account',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'third_party_auth.oauth2providerconfig': {
'Meta': {'object_name': 'OAuth2ProviderConfig'},
'backend_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'secret': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'third_party_auth.samlconfiguration': {
'Meta': {'object_name': 'SAMLConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'entity_id': ('django.db.models.fields.CharField', [], {'default': "'http://saml.example.com'", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_info_str': ('django.db.models.fields.TextField', [], {'default': '\'{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}\''}),
'other_config_str': ('django.db.models.fields.TextField', [], {'default': '\'{\\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\\n}\''}),
'private_key': ('django.db.models.fields.TextField', [], {}),
'public_key': ('django.db.models.fields.TextField', [], {})
},
'third_party_auth.samlproviderconfig': {
'Meta': {'object_name': 'SAMLProviderConfig'},
'attr_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_first_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_full_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_last_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_user_permanent_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'attr_username': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'backend_name': ('django.db.models.fields.CharField', [], {'default': "'tpa-saml'", 'max_length': '50'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'idp_slug': ('django.db.models.fields.SlugField', [], {'max_length': '30'}),
'metadata_source': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'third_party_auth.samlproviderdata': {
'Meta': {'ordering': "('-fetched_at',)", 'object_name': 'SAMLProviderData'},
'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'expires_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'fetched_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'public_key': ('django.db.models.fields.TextField', [], {}),
'sso_url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
}
}
complete_apps = ['third_party_auth']
\ No newline at end of file
......@@ -93,15 +93,6 @@ 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
......
......@@ -77,7 +77,6 @@ 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
......@@ -85,7 +84,6 @@ from logging import getLogger
from . import provider
# These are the query string params you can pass
# to the URL that starts the authentication process.
#
......@@ -194,20 +192,6 @@ 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.
......@@ -554,34 +538,18 @@ 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 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()
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})
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 autosubmit_registration_form():
if should_force_account_creation():
return dispatch_to_register()
return dispatch_to_login()
elif auth_entry in [AUTH_ENTRY_REGISTER, AUTH_ENTRY_REGISTER_2]:
......@@ -625,22 +593,6 @@ 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.
......
......@@ -9,6 +9,7 @@ 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',
......@@ -52,7 +53,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',
'third_party_auth.pipeline.create_user',
'social.pipeline.user.create_user',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details',
......
......@@ -2,16 +2,12 @@
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 .pipeline import AUTH_ENTRY_CUSTOM
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
......@@ -47,47 +43,6 @@ class ConfigurationModelStrategy(DjangoStrategy):
# 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)
def request_host(self):
"""
Host in use for this request
......
......@@ -111,7 +111,7 @@ 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()
......@@ -157,32 +157,8 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
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_custom_form_does_not_link_by_email(self):
self._configure_testshib_provider(autoprovision_account=False)
self._configure_testshib_provider()
self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
email = 'myself@testshib.org'
......@@ -201,7 +177,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
self.assertEqual(testshib_response['Location'], self.url_prefix + '/auth/custom_auth_entry')
def test_custom_form_links_by_email(self):
self._configure_testshib_provider(autoprovision_account=False)
self._configure_testshib_provider()
self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
email = 'myself@testshib.org'
......@@ -229,35 +205,10 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
self.client.logout()
self._test_return_login()
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()
self._test_return_login()
def _test_return_login(self):
""" Test logging in to an account that is already linked. """
# Make sure we're not logged in:
dashboard_response = self.client.get(reverse('dashboard'))
dashboard_response = self.client.get(self.dashboard_page_url)
self.assertEqual(dashboard_response.status_code, 302)
# The user goes to the login page, and sees a button to login with TestShib:
self._check_login_page()
......@@ -276,7 +227,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
self.assertEqual(login_response.status_code, 302)
self.assertEqual(login_response['Location'], self.url_prefix + self.dashboard_page_url)
# Now we are logged in:
dashboard_response = self.client.get(reverse('dashboard'))
dashboard_response = self.client.get(self.dashboard_page_url)
self.assertEqual(dashboard_response.status_code, 200)
def _freeze_time(self, timestamp):
......@@ -311,7 +262,6 @@ 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:
......
"""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
......@@ -45,38 +43,3 @@ class ProviderUserStateTestCase(testutil.TestCase):
google_provider = self.configure_google_provider(enabled=True)
state = pipeline.ProviderUserState(google_provider, object(), None)
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)
......@@ -24,80 +24,6 @@ class TestStrategy(TestCase):
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)
@ddt.data(
(True, None, 'host', 'host'),
(True, "", 'other_host', 'other_host'),
......
......@@ -592,14 +592,6 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
SOCIAL_AUTH_RESPECT_X_FORWARDED_HEADERS = ENV_TOKENS.get('SOCIAL_AUTH_RESPECT_X_FORWARDED_HEADERS')
# 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")
# The following can be used to integrate a custom login form with third_party_auth.
# It should be a dict where the key is a word passed via ?auth_entry=, and the value is a
# dict with an arbitrary 'secret_key' and a 'url'.
......
......@@ -258,9 +258,6 @@ AUTHENTICATION_BACKENDS = (
'third_party_auth.saml.SAMLAuthBackend',
) + AUTHENTICATION_BACKENDS
FAKE_EMAIL_DOMAIN = 'fake-email-domain.foo'
THIRD_PARTY_AUTH_FALLBACK_FULL_NAME = "Unknown"
THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS = {
'custom1': {
'secret_key': 'opensesame',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment