Skip to content

Commit

Permalink
Merge branch 'pipeline'
Browse files Browse the repository at this point in the history
  • Loading branch information
omab committed Oct 17, 2011
2 parents c49c760 + 6784213 commit 6628416
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 182 deletions.
1 change: 0 additions & 1 deletion example/settings.py
Expand Up @@ -79,7 +79,6 @@
'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',
)
Expand Down
229 changes: 53 additions & 176 deletions social_auth/backends/__init__.py
Expand Up @@ -27,18 +27,15 @@
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
Expand Down Expand Up @@ -84,6 +81,15 @@ def _setting(name, default=None):
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):
Expand All @@ -108,168 +114,55 @@ def authenticate(self, *args, **kwargs):
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')
Expand All @@ -284,13 +177,6 @@ def get_user_details(self, response):
"""
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.
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions 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
12 changes: 12 additions & 0 deletions 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
20 changes: 20 additions & 0 deletions 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
52 changes: 52 additions & 0 deletions 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()

0 comments on commit 6628416

Please sign in to comment.