Commit 7f2c956e by Fred Smith

Merge pull request #560 from edx-solutions/rc/2015-11-05

Rc/2015 11 05
parents 15265445 2b0976f5
...@@ -1357,22 +1357,11 @@ def change_setting(request): ...@@ -1357,22 +1357,11 @@ def change_setting(request):
class AccountValidationError(Exception): class AccountValidationError(Exception):
""" Exception thrown if some account validation error happened """
def __init__(self, message, field): def __init__(self, message, field):
super(AccountValidationError, self).__init__(message) super(AccountValidationError, self).__init__(message)
self.field = field 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) @receiver(post_save, sender=User)
def user_signup_handler(sender, **kwargs): # pylint: disable=unused-argument def user_signup_handler(sender, **kwargs): # pylint: disable=unused-argument
""" """
...@@ -1414,12 +1403,12 @@ def _do_create_account(form): ...@@ -1414,12 +1403,12 @@ def _do_create_account(form):
except IntegrityError: except IntegrityError:
# Figure out the cause of the integrity error # Figure out the cause of the integrity error
if len(User.objects.filter(username=user.username)) > 0: 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), _("An account with the Public Username '{username}' already exists.").format(username=user.username),
field="username" field="username"
) )
elif len(User.objects.filter(email=user.email)) > 0: 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), _("An account with the Email '{email}' already exists.").format(email=user.email),
field="email" field="email"
) )
...@@ -1454,7 +1443,7 @@ def _do_create_account(form): ...@@ -1454,7 +1443,7 @@ def _do_create_account(form):
# pylint: disable=too-many-statements # 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 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 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): ...@@ -1548,8 +1537,7 @@ def create_account_with_params(request, params, skip_email=False):
(user, profile, registration) = _do_create_account(form) (user, profile, registration) = _do_create_account(form)
# next, link the account with social auth, if provided via the API. # 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, # (If the user is using the normal register page, the social auth pipeline does the linking, not this code)
# the social auth pipeline does the linking, not this code)
if should_link_with_social_auth: if should_link_with_social_auth:
backend_name = params['provider'] backend_name = params['provider']
request.social_strategy = social_utils.load_strategy(request) request.social_strategy = social_utils.load_strategy(request)
...@@ -1633,12 +1621,11 @@ def create_account_with_params(request, params, skip_email=False): ...@@ -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 # the other for *new* systems. we need to be careful about
# changing settings on a running system to make sure no users are # changing settings on a running system to make sure no users are
# left in an inconsistent state (or doing a migration if they are). # left in an inconsistent state (or doing a migration if they are).
send_email = not ( send_email = (
skip_email or not settings.FEATURES.get('SKIP_EMAIL_VALIDATION', None) and
settings.FEATURES.get('SKIP_EMAIL_VALIDATION', False) or not settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING') and
settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING', False) or not (do_external_auth and settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH')) and
(do_external_auth and settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH')) or not (
(
third_party_provider and third_party_provider.skip_email_verification and third_party_provider and third_party_provider.skip_email_verification and
user.email == running_pipeline['kwargs'].get('details', {}).get('email') 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): ...@@ -93,15 +93,6 @@ class ProviderConfig(ConfigurationModel):
"email, and their account will be activated immediately upon registration." "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 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 backend_name = None # Set to a field or fixed value in subclass
......
...@@ -77,7 +77,6 @@ from social.apps.django_app.default import models ...@@ -77,7 +77,6 @@ from social.apps.django_app.default import models
from social.exceptions import AuthException from social.exceptions import AuthException
from social.pipeline import partial from social.pipeline import partial
from social.pipeline.social_auth import associate_by_email from social.pipeline.social_auth import associate_by_email
from social.pipeline.user import create_user as social_create_user
import student import student
...@@ -85,7 +84,6 @@ from logging import getLogger ...@@ -85,7 +84,6 @@ from logging import getLogger
from . import provider from . import provider
# These are the query string params you can pass # These are the query string params you can pass
# to the URL that starts the authentication process. # to the URL that starts the authentication process.
# #
...@@ -194,20 +192,6 @@ class NotActivatedException(AuthException): ...@@ -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): class ProviderUserState(object):
"""Object representing the provider state (attached or not) for a user. """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 ...@@ -554,34 +538,18 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
"""Redirects to the registration page.""" """Redirects to the registration page."""
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER]) return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER])
def get_provider(): def should_force_account_creation():
""" """ For some third party providers, we auto-create user accounts """
Gets third-party provider for request current_provider = provider.Registry.get_from_pipeline({'backend': backend.name, 'kwargs': kwargs})
"""
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 return current_provider and current_provider.skip_email_verification
if not user: 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]: if auth_entry in [AUTH_ENTRY_LOGIN_API, AUTH_ENTRY_REGISTER_API]:
return HttpResponseBadRequest() return HttpResponseBadRequest()
elif auth_entry in [AUTH_ENTRY_LOGIN, AUTH_ENTRY_LOGIN_2]: 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 # User has authenticated with the third party provider but we don't know which edX
# account corresponds to them yet, if any. # account corresponds to them yet, if any.
if autosubmit_registration_form(): if should_force_account_creation():
return dispatch_to_register() return dispatch_to_register()
return dispatch_to_login() return dispatch_to_login()
elif auth_entry in [AUTH_ENTRY_REGISTER, AUTH_ENTRY_REGISTER_2]: 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 ...@@ -625,22 +593,6 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
@partial.partial @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): 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. """This pipeline step sets the "logged in" cookie for authenticated users.
......
...@@ -9,6 +9,7 @@ If true, it: ...@@ -9,6 +9,7 @@ If true, it:
a) loads this module. a) loads this module.
b) calls apply_settings(), passing in the Django settings b) calls apply_settings(), passing in the Django settings
""" """
_FIELDS_STORED_IN_SESSION = ['auth_entry', 'next'] _FIELDS_STORED_IN_SESSION = ['auth_entry', 'next']
_MIDDLEWARE_CLASSES = ( _MIDDLEWARE_CLASSES = (
'third_party_auth.middleware.ExceptionMiddleware', 'third_party_auth.middleware.ExceptionMiddleware',
...@@ -52,7 +53,7 @@ def apply_settings(django_settings): ...@@ -52,7 +53,7 @@ def apply_settings(django_settings):
'social.pipeline.user.get_username', 'social.pipeline.user.get_username',
'third_party_auth.pipeline.set_pipeline_timeout', 'third_party_auth.pipeline.set_pipeline_timeout',
'third_party_auth.pipeline.ensure_user_information', '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.associate_user',
'social.pipeline.social_auth.load_extra_data', 'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details', 'social.pipeline.user.user_details',
......
...@@ -2,16 +2,12 @@ ...@@ -2,16 +2,12 @@
A custom Strategy for python-social-auth that allows us to fetch configuration from A custom Strategy for python-social-auth that allows us to fetch configuration from
ConfigurationModels rather than django.settings ConfigurationModels rather than django.settings
""" """
import logging
from .models import OAuth2ProviderConfig from .models import OAuth2ProviderConfig
from .pipeline import AUTH_ENTRY_CUSTOM from .pipeline import AUTH_ENTRY_CUSTOM
from social.backends.oauth import BaseOAuth2 from social.backends.oauth import BaseOAuth2
from social.strategies.django_strategy import DjangoStrategy from social.strategies.django_strategy import DjangoStrategy
log = logging.getLogger(__name__)
class ConfigurationModelStrategy(DjangoStrategy): class ConfigurationModelStrategy(DjangoStrategy):
""" """
A DjangoStrategy customized to load settings from ConfigurationModels A DjangoStrategy customized to load settings from ConfigurationModels
...@@ -47,47 +43,6 @@ class ConfigurationModelStrategy(DjangoStrategy): ...@@ -47,47 +43,6 @@ class ConfigurationModelStrategy(DjangoStrategy):
# It's probably a global Django setting like 'FIELDS_STORED_IN_SESSION': # It's probably a global Django setting like 'FIELDS_STORED_IN_SESSION':
return super(ConfigurationModelStrategy, self).setting(name, default, backend) 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): def request_host(self):
""" """
Host in use for this request Host in use for this request
......
...@@ -111,7 +111,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): ...@@ -111,7 +111,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL)
# And we should be redirected to the dashboard: # And we should be redirected to the dashboard:
self.assertEqual(continue_response.status_code, 302) 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: # Now check that we can login again:
self.client.logout() self.client.logout()
...@@ -157,32 +157,8 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): ...@@ -157,32 +157,8 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
self.client.logout() self.client.logout()
self._test_return_login() 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): 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. self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
email = 'myself@testshib.org' email = 'myself@testshib.org'
...@@ -201,7 +177,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): ...@@ -201,7 +177,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
self.assertEqual(testshib_response['Location'], self.url_prefix + '/auth/custom_auth_entry') self.assertEqual(testshib_response['Location'], self.url_prefix + '/auth/custom_auth_entry')
def test_custom_form_links_by_email(self): 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. self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
email = 'myself@testshib.org' email = 'myself@testshib.org'
...@@ -229,35 +205,10 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): ...@@ -229,35 +205,10 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
self.client.logout() self.client.logout()
self._test_return_login() 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): def _test_return_login(self):
""" Test logging in to an account that is already linked. """ """ Test logging in to an account that is already linked. """
# Make sure we're not logged in: # 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) self.assertEqual(dashboard_response.status_code, 302)
# The user goes to the login page, and sees a button to login with TestShib: # The user goes to the login page, and sees a button to login with TestShib:
self._check_login_page() self._check_login_page()
...@@ -276,7 +227,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): ...@@ -276,7 +227,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
self.assertEqual(login_response.status_code, 302) self.assertEqual(login_response.status_code, 302)
self.assertEqual(login_response['Location'], self.url_prefix + self.dashboard_page_url) self.assertEqual(login_response['Location'], self.url_prefix + self.dashboard_page_url)
# Now we are logged in: # 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) self.assertEqual(dashboard_response.status_code, 200)
def _freeze_time(self, timestamp): def _freeze_time(self, timestamp):
...@@ -311,7 +262,6 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): ...@@ -311,7 +262,6 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
kwargs.setdefault('metadata_source', TESTSHIB_METADATA_URL) kwargs.setdefault('metadata_source', TESTSHIB_METADATA_URL)
kwargs.setdefault('icon_class', 'fa-university') 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('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) self.configure_saml_provider(**kwargs)
if fetch_metadata: if fetch_metadata:
......
"""Unit tests for third_party_auth/pipeline.py.""" """Unit tests for third_party_auth/pipeline.py."""
import random import random
import mock
from student.views import AccountEmailAlreadyExistsValidationError
from third_party_auth import pipeline, provider from third_party_auth import pipeline, provider
from third_party_auth.tests import testutil from third_party_auth.tests import testutil
import unittest import unittest
...@@ -45,38 +43,3 @@ class ProviderUserStateTestCase(testutil.TestCase): ...@@ -45,38 +43,3 @@ class ProviderUserStateTestCase(testutil.TestCase):
google_provider = self.configure_google_provider(enabled=True) google_provider = self.configure_google_provider(enabled=True)
state = pipeline.ProviderUserState(google_provider, object(), None) state = pipeline.ProviderUserState(google_provider, object(), None)
self.assertEqual(google_provider.provider_id + '_unlink_form', state.get_unlink_form_name()) 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): ...@@ -24,80 +24,6 @@ class TestStrategy(TestCase):
args, unused_kwargs = patched_create_account.call_args args, unused_kwargs = patched_create_account.call_args
return 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( @ddt.data(
(True, None, 'host', 'host'), (True, None, 'host', 'host'),
(True, "", 'other_host', 'other_host'), (True, "", 'other_host', 'other_host'),
......
...@@ -592,14 +592,6 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): ...@@ -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') 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. # 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 # 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'. # dict with an arbitrary 'secret_key' and a 'url'.
......
...@@ -258,9 +258,6 @@ AUTHENTICATION_BACKENDS = ( ...@@ -258,9 +258,6 @@ AUTHENTICATION_BACKENDS = (
'third_party_auth.saml.SAMLAuthBackend', 'third_party_auth.saml.SAMLAuthBackend',
) + AUTHENTICATION_BACKENDS ) + AUTHENTICATION_BACKENDS
FAKE_EMAIL_DOMAIN = 'fake-email-domain.foo'
THIRD_PARTY_AUTH_FALLBACK_FULL_NAME = "Unknown"
THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS = { THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS = {
'custom1': { 'custom1': {
'secret_key': 'opensesame', 'secret_key': 'opensesame',
......
# Custom requirements to be customized by individual OpenEdX instances # Custom requirements to be customized by individual OpenEdX instances
# When updating a hash of an XBlock that users xblock-utils, please update its version hash in github.txt # When updating a hash of an XBlock that uses xblock-utils, please update its version hash in github.txt.
-e git+https://github.com/edx/xblock-utils.git@3b58c757f06943072b170654d676e95b9adb37b0#egg=xblock-utils
-e git+https://github.com/edx-solutions/xblock-mentoring.git@bd0b3f413ae7e8274985555adfd7de7af3eca84c#egg=xblock-mentoring -e git+https://github.com/edx-solutions/xblock-mentoring.git@bd0b3f413ae7e8274985555adfd7de7af3eca84c#egg=xblock-mentoring
-e git+https://github.com/edx-solutions/xblock-image-explorer.git@21b9bcc4f2c7917463ab18a596161ac6c58c9c4a#egg=xblock-image-explorer -e git+https://github.com/edx-solutions/xblock-image-explorer.git@21b9bcc4f2c7917463ab18a596161ac6c58c9c4a#egg=xblock-image-explorer
-e git+https://github.com/edx-solutions/xblock-drag-and-drop.git@92ee2055a16899090a073e1df81e35d5293ad767#egg=xblock-drag-and-drop -e git+https://github.com/edx-solutions/xblock-drag-and-drop.git@92ee2055a16899090a073e1df81e35d5293ad767#egg=xblock-drag-and-drop
...@@ -9,9 +8,9 @@ ...@@ -9,9 +8,9 @@
-e git+https://github.com/edx-solutions/xblock-ooyala.git@42f769d422850df81bcbd2dbcc344f86b6a17d8e#egg=xblock-ooyala -e git+https://github.com/edx-solutions/xblock-ooyala.git@42f769d422850df81bcbd2dbcc344f86b6a17d8e#egg=xblock-ooyala
-e git+https://github.com/edx-solutions/xblock-group-project.git@6b3393a1a5eb76224ecd3311e870ab8adf4badbf#egg=xblock-group-project -e git+https://github.com/edx-solutions/xblock-group-project.git@6b3393a1a5eb76224ecd3311e870ab8adf4badbf#egg=xblock-group-project
-e git+https://github.com/edx-solutions/xblock-adventure.git@effa22006bb6528bc6d3788787466eb4e74e1161#egg=xblock-adventure -e git+https://github.com/edx-solutions/xblock-adventure.git@effa22006bb6528bc6d3788787466eb4e74e1161#egg=xblock-adventure
-e git+https://github.com/mckinseyacademy/xblock-poll.git@ca0e6eb4ef10c128d573c3cec015dcfee7984730#egg=xblock-poll -e git+https://github.com/open-craft/xblock-poll.git@ed7f32a570fb29a18cce57b14f322667a5f33cd5#egg=xblock-poll
-e git+https://github.com/edx/edx-notifications.git@275b8354593048ecae3e06642985b702b81140cc#egg=edx-notifications -e git+https://github.com/edx/edx-notifications.git@275b8354593048ecae3e06642985b702b81140cc#egg=edx-notifications
-e git+https://github.com/open-craft/problem-builder.git@c6e606027155d92ca78e96e6bc23ea86dcc588fc#egg=problem-builder -e git+https://github.com/open-craft/problem-builder.git@ff271b2dcf8098cc3bfed424244c76ea2c53cc96#egg=problem-builder
-e git+https://github.com/open-craft/xblock-group-project-v2.git@533a3d70b8ff58af0c4f5e22abc674c60881cea3#egg=xblock-group-project-v2 -e git+https://github.com/open-craft/xblock-group-project-v2.git@da8ab1e0e110cb3d2525157958a88f101fd1abdb#egg=xblock-group-project-v2
-e git+https://github.com/OfficeDev/xblock-officemix.git@86238f5968a08db005717dbddc346808f1ed3716#egg=xblock-officemix -e git+https://github.com/OfficeDev/xblock-officemix.git@86238f5968a08db005717dbddc346808f1ed3716#egg=xblock-officemix
-e git+https://github.com/edx-solutions/xblock.git@80d11e883cb0f4b554e1e566294cb7de383cffed#egg=xblock -e git+https://github.com/edx-solutions/xblock.git@80d11e883cb0f4b554e1e566294cb7de383cffed#egg=xblock
...@@ -50,8 +50,8 @@ git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3 ...@@ -50,8 +50,8 @@ git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
-e git+https://github.com/edx/edx-search.git@release-2015-07-03#egg=edx-search -e git+https://github.com/edx/edx-search.git@release-2015-07-03#egg=edx-search
-e git+https://github.com/edx/edx-milestones.git@release-2015-06-17#egg=edx-milestones -e git+https://github.com/edx/edx-milestones.git@release-2015-06-17#egg=edx-milestones
git+https://github.com/edx/edx-lint.git@ed8c8d2a0267d4d42f43642d193e25f8bd575d9b#egg=edx_lint==0.2.3 git+https://github.com/edx/edx-lint.git@ed8c8d2a0267d4d42f43642d193e25f8bd575d9b#egg=edx_lint==0.2.3
# Note for the next rebase: custom.txt or one of XBlocks installed there might require a newer version of xblock-utils - please check versions # Note for the next rebase: one of XBlocks in custom.txt might require a newer version of xblock-utils - please check versions.
-e git+https://github.com/edx/xblock-utils.git@588f7fd3ee88847c57cf09d10e81caa6b267ec51#egg=xblock-utils -e git+https://github.com/edx/xblock-utils.git@08e5a5a9fc8ab46b627435427fd7e04c20809009#egg=xblock-utils
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx/edx-reverification-block.git@a286e89c73e1b788e35ac5b08a54b71a9fa63cfd#egg=edx-reverification-block -e git+https://github.com/edx/edx-reverification-block.git@a286e89c73e1b788e35ac5b08a54b71a9fa63cfd#egg=edx-reverification-block
git+https://github.com/edx/ecommerce-api-client.git@1.0.0#egg=ecommerce-api-client==1.0.0 git+https://github.com/edx/ecommerce-api-client.git@1.0.0#egg=ecommerce-api-client==1.0.0
......
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