SignatureMethod_HMAC_SHA1
from django.conf import settings
-from django.core.exceptions import MultipleObjectsReturned
from django.contrib.auth import authenticate
from django.contrib.auth.backends import ModelBackend
from django.utils import simplejson
from django.utils.importlib import import_module
-from django.db.utils import IntegrityError
from social_auth.models import UserSocialAuth
from social_auth.store import DjangoOpenIDStore
-from social_auth.signals import pre_update, socialauth_registered, \
- socialauth_not_registered
-from social_auth.utils import sanitize_log_data
+from social_auth.signals import pre_update, socialauth_registered
+from social_auth.backends.exceptions import StopPipeline
# OpenID configuration
DEFAULT_USERNAME = _setting('SOCIAL_AUTH_DEFAULT_USERNAME')
CHANGE_SIGNAL_ONLY = _setting('SOCIAL_AUTH_CHANGE_SIGNAL_ONLY', False)
UUID_LENGHT = _setting('SOCIAL_AUTH_UUID_LENGTH', 16)
+PIPELINE = _setting('SOCIAL_AUTH_PIPELINE', (
+ 'social_auth.backends.pipeline.social.social_auth_user',
+ 'social_auth.backends.pipeline.associate.associate_by_email',
+ 'social_auth.backends.pipeline.user.get_username',
+ 'social_auth.backends.pipeline.user.create_user',
+ 'social_auth.backends.pipeline.social.associate_user',
+ 'social_auth.backends.pipeline.social.load_extra_data',
+ 'social_auth.backends.pipeline.user.update_user_details',
+ ))
class SocialAuthBackend(ModelBackend):
response = kwargs.get('response')
details = self.get_user_details(response)
uid = self.get_user_id(details, response)
- is_new = False
user = kwargs.get('user')
-
- try:
- social_user = self.get_social_auth_user(uid)
- except UserSocialAuth.DoesNotExist:
- if user is None: # new user
- if not CREATE_USERS or not kwargs.get('create_user', True):
- # Send signal for cases where tracking failed registering
- # is useful.
- socialauth_not_registered.send(sender=self.__class__,
- uid=uid,
- response=response,
- details=details)
- return None
-
- email = details.get('email')
- if email and ASSOCIATE_BY_MAIL:
- # try to associate accounts registered with the same email
- # address, only if it's a single object. ValueError is
- # raised if multiple objects are returned
- try:
- user = User.objects.get(email=email)
- except MultipleObjectsReturned:
- raise ValueError('Not unique email address supplied')
- except User.DoesNotExist:
- user = None
- if not user:
- username = self.username(details)
- logger.debug('Creating new user with username %s and email %s',
- username, sanitize_log_data(email))
- user = User.objects.create_user(username=username,
- email=email)
- is_new = True
-
- try:
- social_user = self.associate_auth(user, uid, response, details)
- except IntegrityError:
- # Protect for possible race condition, those bastard with FTL
- # clicking capabilities
- social_user = self.get_social_auth_user(uid)
-
- # Raise ValueError if this account was registered by another user.
- if user and user != social_user.user:
- raise ValueError('Account already in use.', social_user)
- user = social_user.user
-
- # Flag user "new" status
- setattr(user, 'is_new', is_new)
-
- # Update extra_data storage, unless disabled by setting
- if LOAD_EXTRA_DATA:
- extra_data = self.extra_data(user, uid, response, details)
- if extra_data and social_user.extra_data != extra_data:
- social_user.extra_data = extra_data
- social_user.save()
-
- user.social_user = social_user
-
- # Update user account data.
- self.update_user_details(user, response, details, is_new)
- return user
-
- def username(self, details):
- """Return an unique username, if SOCIAL_AUTH_FORCE_RANDOM_USERNAME
- setting is True, then username will be a random USERNAME_MAX_LENGTH
- chars uuid generated hash
- """
- def mk_uuid():
- """Return hash from unique string"""
- return uuid4().get_hex()
-
- if FORCE_RANDOM_USERNAME:
- username = mk_uuid()
- elif details.get(USERNAME):
- username = details[USERNAME]
- elif DEFAULT_USERNAME:
- username = DEFAULT_USERNAME
- if callable(username):
- username = username()
- else:
- username = mk_uuid()
-
- short_username = username[:USERNAME_MAX_LENGTH - UUID_LENGHT]
- final_username = None
-
- while True:
- final_username = USERNAME_FIXER(username)[:USERNAME_MAX_LENGTH]
-
+ request = kwargs.get('request')
+
+ # Pipeline:
+ # Arguments:
+ # request, backend, social_user, uid, response, details
+ # user, is_new, args, kwargs
+ kwargs = kwargs.copy()
+ kwargs.update({
+ 'backend': self,
+ 'request': request,
+ 'uid': uid,
+ 'user': user,
+ 'social_user': None,
+ 'response': response,
+ 'details': details,
+ 'is_new': False,
+ })
+ for name in PIPELINE:
try:
- User.objects.get(username=final_username)
- except User.DoesNotExist:
- break
- else:
- # User with same username already exists, generate a unique
- # username for current user using username as base but adding
- # a unique hash at the end. Original username is cut to avoid
- # the field max_length.
- username = short_username + mk_uuid()[:UUID_LENGHT]
-
- return final_username
-
- def associate_auth(self, user, uid, response, details):
- """Associate a Social Auth with an user account."""
- return UserSocialAuth.objects.create(user=user, uid=uid,
- provider=self.name)
+ mod_name, func_name = name.rsplit('.', 1)
+ try:
+ mod = import_module(mod_name)
+ except ImportError:
+ print "IMPORT ERROR", mod_name, func_name
+ logger.exception('Error importing pipeline %s', name)
+ else:
+ pipeline = getattr(mod, func_name, None)
+ if callable(pipeline):
+ print "CALLABLE", mod_name, func_name
+ try:
+ kwargs.update(pipeline(*args, **kwargs) or {})
+ except StopPipeline:
+ break
+ except Exception, e:
+ print "EXCEPTION:", str(e)
+
+ social_user = kwargs.get('social_user')
+ if social_user:
+ # define user.social_user attribute to track current social
+ # account
+ user = social_user.user
+ user.social_user = social_user
+ return user
def extra_data(self, user, uid, response, details):
"""Return default blank user extra data"""
return ''
- def update_user_details(self, user, response, details, is_new=False):
- """Update user details with (maybe) new data. Username is not
- changed if associating a new credential."""
- changed = False # flag to track changes
-
- # check if values update should be left to signals handlers only
- if not CHANGE_SIGNAL_ONLY:
- logger.debug('Updating user details for user %s', user,
- extra=dict(data=details))
-
- for name, value in details.iteritems():
- # do not update username, it was already generated by
- # self.username(...) and loaded in given instance
- if name != USERNAME and value and value != getattr(user, name,
- None):
- setattr(user, name, value)
- changed = True
-
- # Fire a pre-update signal sending current backend instance,
- # user instance (created or retrieved from database), service
- # response and processed details.
- #
- # Also fire socialauth_registered signal for newly registered
- # users.
- #
- # Signal handlers must return True or False to signal instance
- # changes. Send method returns a list of tuples with receiver
- # and it's response.
- signal_response = lambda (receiver, response): response
-
- kwargs = {'sender': self.__class__, 'user': user,
- 'response': response, 'details': details}
- changed |= any(filter(signal_response, pre_update.send(**kwargs)))
-
- # Fire socialauth_registered signal on new user registration
- if is_new:
- changed |= any(filter(signal_response,
- socialauth_registered.send(**kwargs)))
-
- if changed:
- user.save()
-
- def get_social_auth_user(self, uid):
- """Return social auth user instance for given uid for current
- backend.
-
- Riase DoesNotExist exception if no entry.
- """
- return UserSocialAuth.objects.select_related('user')\
- .get(provider=self.name, uid=uid)
-
def get_user_id(self, details, response):
"""Must return a unique ID from values returned on details"""
raise NotImplementedError('Implement in subclass')
"""
raise NotImplementedError('Implement in subclass')
- def get_user(self, user_id):
- """Return user instance for @user_id"""
- try:
- return User.objects.get(pk=user_id)
- except User.DoesNotExist:
- return None
-
class OAuthBackend(SocialAuthBackend):
"""OAuth authentication backend base class.
"""Generic OpenID authentication backend"""
name = 'openid'
- def get_social_auth_user(self, uid):
- """Return social auth user instance for given uid. OpenId uses
- identity_url to identify the user in a unique way and that value
- identifies the provider too.
-
- Riase DoesNotExist exception if no entry.
- """
- return UserSocialAuth.objects.select_related('user').get(uid=uid)
-
def get_user_id(self, details, response):
"""Return user unique id provided by service"""
return response.identity_url
--- /dev/null
+from django.conf import settings
+from django.db.utils import IntegrityError
+
+from social_auth.models import User, UserSocialAuth
+
+
+def social_auth_user(backend, uid, user=None, *args, **kwargs):
+ """Return UserSocialAuth account for backend/uid pair or None if it
+ doesn't exists.
+
+ Raise ValueError if UserSocialAuth entry belongs to another user.
+ """
+ try:
+ social_user = UserSocialAuth.objects.select_related('user')\
+ .get(provider=backend.name,
+ uid=uid)
+ except UserSocialAuth.DoesNotExist:
+ social_user = None
+
+ if user and social_user and social_user.user != user:
+ raise ValueError('Account already in use.', social_user)
+ return {'social_user': social_user}
+
+
+def associate_user(backend, user, uid, social_user=None, *args, **kwargs):
+ """Associate user social account with user instance."""
+ if social_user:
+ return None
+
+ try:
+ social = UserSocialAuth.objects.create(user=user, uid=uid,
+ provider=backend.name)
+ except IntegrityError:
+ # Protect for possible race condition, those bastard with FTL
+ # clicking capabilities, check issue #131:
+ # https://github.com/omab/django-social-auth/issues/131
+ return social_auth_user(backend, uid, user, social_user=social_user,
+ *args, **kwargs)
+ else:
+ return {'social_user': social}
+
+
+def load_extra_data(backend, details, response, social_user, uid, user,
+ *args, **kwargs):
+ """Load extra data from provider and store it on current UserSocialAuth
+ extra_data field.
+ """
+ if getattr(settings, 'SOCIAL_AUTH_EXTRA_DATA', True):
+ extra_data = backend.extra_data(user, uid, response, details)
+ if extra_data and social_user.extra_data != extra_data:
+ social_user.extra_data = extra_data
+ social_user.save()
--- /dev/null
+from uuid import uuid4
+
+from django.conf import settings
+
+from social_auth.models import User
+from social_auth.backends.pipeline import USERNAME, USERNAME_MAX_LENGTH
+from social_auth.signals import socialauth_not_registered, \
+ socialauth_registered, \
+ pre_update
+
+
+def get_username(details, user=None, *args, **kwargs):
+ """Return an username for new user. Return current user username
+ if user was given.
+ """
+ if user:
+ return {'username': user.username}
+
+ FORCE_RANDOM_USERNAME = getattr(settings,
+ 'SOCIAL_AUTH_FORCE_RANDOM_USERNAME',
+ False)
+ if FORCE_RANDOM_USERNAME:
+ username = uuid4().get_hex()
+ elif details.get(USERNAME):
+ username = details[USERNAME]
+ elif settings.hasattr('SOCIAL_AUTH_DEFAULT_USERNAME'):
+ username = settings.SOCIAL_AUTH_DEFAULT_USERNAME
+ if callable(username):
+ username = username()
+ else:
+ username = uuid4().get_hex()
+
+ uuid_lenght = getattr(settings, 'SOCIAL_AUTH_UUID_LENGTH', 16)
+ username_fixer = getattr(settings, 'SOCIAL_AUTH_USERNAME_FIXER',
+ lambda u: u)
+
+ short_username = username[:USERNAME_MAX_LENGTH - uuid_lenght]
+ final_username = None
+
+ while True:
+ final_username = username_fixer(username)[:USERNAME_MAX_LENGTH]
+
+ try:
+ User.objects.get(username=final_username)
+ except User.DoesNotExist:
+ break
+ else:
+ # User with same username already exists, generate a unique
+ # username for current user using username as base but adding
+ # a unique hash at the end. Original username is cut to avoid
+ # the field max_length.
+ username = short_username + uuid4().get_hex()[:uuid_lenght]
+ return {'username': final_username}
+
+
+def create_user(backend, details, response, uid, username, user=None, *args, **kwargs):
+ """Create user. Depends on get_username pipeline."""
+ if user:
+ return {'user': user}
+ if not username:
+ return None
+
+ if not getattr(settings, 'SOCIAL_AUTH_CREATE_USERS', True):
+ # Send signal for cases where tracking failed registering is useful.
+ socialauth_not_registered.send(sender=backend.__class__,
+ uid=uid,
+ response=response,
+ details=details)
+ return None
+
+ email = details.get('email')
+ return {
+ 'user': User.objects.create_user(username=username, email=email),
+ 'is_new': True
+ }
+
+
+def update_user_details(backend, details, response, user, is_new=False, *args, **kwargs):
+ """Update user details using data from provider."""
+ changed = False # flag to track changes
+
+ # check if values update should be left to signals handlers only
+ if not getattr(settings, 'SOCIAL_AUTH_CHANGE_SIGNAL_ONLY', False):
+ for name, value in details.iteritems():
+ # do not update username, it was already generated
+ if name == USERNAME:
+ continue
+ if value and value != getattr(user, name, None):
+ setattr(user, name, value)
+ changed = True
+
+ # Fire a pre-update signal sending current backend instance,
+ # user instance (created or retrieved from database), service
+ # response and processed details.
+ #
+ # Also fire socialauth_registered signal for newly registered
+ # users.
+ #
+ # Signal handlers must return True or False to signal instance
+ # changes. Send method returns a list of tuples with receiver
+ # and it's response.
+ signal_response = lambda (receiver, response): response
+ signal_kwargs = {'sender': backend.__class__, 'user': user,
+ 'response': response, 'details': details}
+
+ changed |= any(filter(signal_response, pre_update.send(**signal_kwargs)))
+
+ # Fire socialauth_registered signal on new user registration
+ if is_new:
+ changed |= any(filter(signal_response,
+ socialauth_registered.send(**signal_kwargs)))
+
+ if changed:
+ user.save()