]> git.parisson.com Git - django-social-auth.git/commitdiff
Pipeline. Refs gh-90
authorMatías Aguirre <matiasaguirre@gmail.com>
Mon, 17 Oct 2011 05:57:57 +0000 (03:57 -0200)
committerMatías Aguirre <matiasaguirre@gmail.com>
Mon, 17 Oct 2011 05:57:57 +0000 (03:57 -0200)
example/settings.py
social_auth/backends/__init__.py
social_auth/backends/exceptions.py [new file with mode: 0644]
social_auth/backends/pipeline/__init__.py [new file with mode: 0644]
social_auth/backends/pipeline/associate.py [new file with mode: 0644]
social_auth/backends/pipeline/social.py [new file with mode: 0644]
social_auth/backends/pipeline/user.py [new file with mode: 0644]
social_auth/views.py

index 93267dfa4aef73ffcdece7f12f34ba7289eaf364..9a42bd2a3664348ab51927d6411ef93c9d59ecf6 100644 (file)
@@ -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',
 )
index 3cf23bba7cf15836916581c049e0fce3759eadeb..434d24c85360d76ba1fe1d16a865361e7de740a1 100644 (file)
@@ -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 (file)
index 0000000..8518b31
--- /dev/null
@@ -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 (file)
index 0000000..360545b
--- /dev/null
@@ -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 (file)
index 0000000..c605dda
--- /dev/null
@@ -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 (file)
index 0000000..d7bb6f1
--- /dev/null
@@ -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 (file)
index 0000000..a7c5298
--- /dev/null
@@ -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()
index 4de8b8d73c02277d80fa3acb38b3cce1a8f9eabf..aa69cd89f5b9068ac6d91a00875ec4f471cb3037 100644 (file)
@@ -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)