From: Matías Aguirre Date: Mon, 17 Oct 2011 05:57:57 +0000 (-0200) Subject: Pipeline. Refs gh-90 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=6784213f92fca5ef17bfcc5ce1ec07dae7f181a0;p=django-social-auth.git Pipeline. Refs gh-90 --- diff --git a/example/settings.py b/example/settings.py index 93267df..9a42bd2 100644 --- a/example/settings.py +++ b/example/settings.py @@ -79,7 +79,6 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.debug', 'django.core.context_processors.i18n', 'django.core.context_processors.media', - 'django.core.context_processors.static', 'django.contrib.messages.context_processors.messages', 'social_auth.context_processors.social_auth_by_type_backends', ) diff --git a/social_auth/backends/__init__.py b/social_auth/backends/__init__.py index 3cf23bb..434d24c 100644 --- a/social_auth/backends/__init__.py +++ b/social_auth/backends/__init__.py @@ -27,18 +27,15 @@ from oauth2 import Consumer as OAuthConsumer, Token, Request as OAuthRequest, \ 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 @@ -84,6 +81,15 @@ USERNAME_FIXER = _setting('SOCIAL_AUTH_USERNAME_FIXER', lambda u: u) 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): @@ -108,168 +114,55 @@ 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') @@ -284,13 +177,6 @@ class SocialAuthBackend(ModelBackend): """ 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. @@ -325,15 +211,6 @@ class OpenIDBackend(SocialAuthBackend): """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 diff --git a/social_auth/backends/exceptions.py b/social_auth/backends/exceptions.py new file mode 100644 index 0000000..8518b31 --- /dev/null +++ b/social_auth/backends/exceptions.py @@ -0,0 +1,5 @@ +class StopPipeline(Exception): + """Stop pipeline process exception. + Raise this exception to stop the rest of the pipeline process. + """ + pass diff --git a/social_auth/backends/pipeline/__init__.py b/social_auth/backends/pipeline/__init__.py new file mode 100644 index 0000000..360545b --- /dev/null +++ b/social_auth/backends/pipeline/__init__.py @@ -0,0 +1,12 @@ +"""Django-Social-Auth Pipeline. + +Pipelines must return a dictionary with values that will be passed as parameter +to next pipeline item. Pipelines must take **kwargs parameters to avoid +failure. At some point a pipeline entry must create a UserSocialAuth instance +and load it to the output if the user logged in correctly. +""" +from social_auth.models import User + + +USERNAME = 'username' +USERNAME_MAX_LENGTH = User._meta.get_field(USERNAME).max_length diff --git a/social_auth/backends/pipeline/associate.py b/social_auth/backends/pipeline/associate.py new file mode 100644 index 0000000..c605dda --- /dev/null +++ b/social_auth/backends/pipeline/associate.py @@ -0,0 +1,20 @@ +from django.conf import settings +from django.core.exceptions import MultipleObjectsReturned + +from social_auth.models import User + + +def associate_by_email(details, *args, **kwargs): + """Return user entry with same email address as one returned on details.""" + email = details.get('email') + + if email and getattr(settings, 'SOCIAL_AUTH_ASSOCIATE_BY_MAIL', False): + # 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: + return {'user': User.objects.get(email=email)} + except MultipleObjectsReturned: + raise ValueError('Not unique email address.') + except User.DoesNotExist: + pass diff --git a/social_auth/backends/pipeline/social.py b/social_auth/backends/pipeline/social.py new file mode 100644 index 0000000..d7bb6f1 --- /dev/null +++ b/social_auth/backends/pipeline/social.py @@ -0,0 +1,52 @@ +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() diff --git a/social_auth/backends/pipeline/user.py b/social_auth/backends/pipeline/user.py new file mode 100644 index 0000000..a7c5298 --- /dev/null +++ b/social_auth/backends/pipeline/user.py @@ -0,0 +1,114 @@ +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() diff --git a/social_auth/views.py b/social_auth/views.py index 4de8b8d..aa69cd8 100644 --- a/social_auth/views.py +++ b/social_auth/views.py @@ -70,12 +70,9 @@ def dsa_view(redirect_name=None): logger.error(unicode(e), exc_info=True, extra=dict(request=request)) - # Why!? - msg = str(e) - if 'django.contrib.messages' in settings.INSTALLED_APPS: from django.contrib.messages.api import error - error(request, msg, extra_tags=backend_name) + error(request, unicode(e), extra_tags=backend_name) else: logger.warn('Messages framework not in place, some '+ 'errors have not been shown to the user.') @@ -185,4 +182,5 @@ def auth_complete(request, backend, user=None, *args, **kwargs): """Complete auth process. Return authenticated user or None.""" if user and not user.is_authenticated(): user = None - return backend.auth_complete(user=user, *args, **kwargs) + kwargs.update({'user': user, 'request': request}) + return backend.auth_complete(*args, **kwargs)