From: Steven Cummings Date: Fri, 22 Jun 2012 19:23:35 +0000 (-0500) Subject: Implemented working alternate storage backend, based on MongoEngine. X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=e06b97a667db9ce36e0a7f5ac1d551750b2112d3;p=django-social-auth.git Implemented working alternate storage backend, based on MongoEngine. It doesn't necessarily need to live in the primary codebase, it is simply a proof of the original idea. With the appearance of a fair number of helper methods to decouple social auth from django models, it really is looking like a backend is in order. More details: + Add MongoEngine models * Add several methods to both models modules to decouple social auth core and default pipelines from Django ORM code * In social pipelines, make sure uid is a str value * Only load django admin settings if current models are django models + Add a conf module to manage loading specific Django settings and default values to use --- diff --git a/social_auth/admin.py b/social_auth/admin.py index ee42c41..fad8d78 100644 --- a/social_auth/admin.py +++ b/social_auth/admin.py @@ -1,31 +1,38 @@ """Admin settings""" -from django.contrib import admin -from social_auth.models import UserSocialAuth, Nonce, Association +from social_auth import conf -class UserSocialAuthOption(admin.ModelAdmin): - """Social Auth user options""" - list_display = ('id', 'user', 'provider', 'uid') - search_fields = ('user__first_name', 'user__last_name', 'user__email') - list_filter = ('provider',) - raw_id_fields = ('user',) - list_select_related = True +if conf.get_models_module().NAME == 'django_models': -class NonceOption(admin.ModelAdmin): - """Nonce options""" - list_display = ('id', 'server_url', 'timestamp', 'salt') - search_fields = ('server_url',) + from django.contrib import admin + from social_auth.models import UserSocialAuth, Nonce, Association -class AssociationOption(admin.ModelAdmin): - """Association options""" - list_display = ('id', 'server_url', 'assoc_type') - list_filter = ('assoc_type',) - search_fields = ('server_url',) + class UserSocialAuthOption(admin.ModelAdmin): + """Social Auth user options""" + list_display = ('id', 'user', 'provider', 'uid') + search_fields = ('user__first_name', 'user__last_name', 'user__email') + list_filter = ('provider',) + raw_id_fields = ('user',) + list_select_related = True -admin.site.register(UserSocialAuth, UserSocialAuthOption) -admin.site.register(Nonce, NonceOption) -admin.site.register(Association, AssociationOption) + + class NonceOption(admin.ModelAdmin): + """Nonce options""" + list_display = ('id', 'server_url', 'timestamp', 'salt') + search_fields = ('server_url',) + + + class AssociationOption(admin.ModelAdmin): + """Association options""" + list_display = ('id', 'server_url', 'assoc_type') + list_filter = ('assoc_type',) + search_fields = ('server_url',) + + + admin.site.register(UserSocialAuth, UserSocialAuthOption) + admin.site.register(Nonce, NonceOption) + admin.site.register(Association, AssociationOption) diff --git a/social_auth/backends/__init__.py b/social_auth/backends/__init__.py index fb439a4..59c4751 100644 --- a/social_auth/backends/__init__.py +++ b/social_auth/backends/__init__.py @@ -25,6 +25,7 @@ from django.contrib.auth.backends import ModelBackend from django.utils import simplejson from django.utils.importlib import import_module +from social_auth.models import get_social_auth_for_user from social_auth.utils import setting, log, model_to_ctype, ctype_to_model, \ clean_partial_pipeline from social_auth.store import DjangoOpenIDStore @@ -35,10 +36,9 @@ from social_auth.backends.exceptions import StopPipeline, AuthException, \ from social_auth.backends.utils import build_consumer_oauth_request -if setting('SOCIAL_AUTH_USER_MODEL'): - User = models.get_model(*setting('SOCIAL_AUTH_USER_MODEL').rsplit('.', 1)) -else: - from django.contrib.auth.models import User +def get_user_model(): + from social_auth.models import User + return User # OpenID configuration @@ -191,12 +191,14 @@ class SocialAuthBackend(ModelBackend): return {} def get_user(self, user_id): + """ Return user with given ID from the User model used by this backend """ + user_cls = get_user_model() try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: + return user_cls.objects.get(id=user_id) + except user_cls.DoesNotExist: return None @@ -412,9 +414,9 @@ class BaseAuth(object): Override if extra operations are needed. """ if association_id: - user.social_auth.get(id=association_id).delete() + get_social_auth_for_user(user).get(id=association_id).delete() else: - user.social_auth.filter(provider=self.AUTH_BACKEND.name).delete() + get_social_auth_for_user(user).filter(provider=self.AUTH_BACKEND.name).delete() def build_absolute_uri(self, path=None): """Build absolute URI for given path. Replace http:// schema with diff --git a/social_auth/backends/pipeline/__init__.py b/social_auth/backends/pipeline/__init__.py index ae04cde..2866a99 100644 --- a/social_auth/backends/pipeline/__init__.py +++ b/social_auth/backends/pipeline/__init__.py @@ -10,11 +10,11 @@ import warnings from django.conf import settings from social_auth.models import User +from social_auth.models import USERNAME, get_username_max_length from social_auth.backends import get_backend, PIPELINE -USERNAME = 'username' -USERNAME_MAX_LENGTH = User._meta.get_field(USERNAME).max_length +USERNAME_MAX_LENGTH = get_username_max_length() def warn_setting(name, func_name): diff --git a/social_auth/backends/pipeline/associate.py b/social_auth/backends/pipeline/associate.py index aafd2d3..fd7c850 100644 --- a/social_auth/backends/pipeline/associate.py +++ b/social_auth/backends/pipeline/associate.py @@ -1,5 +1,3 @@ -from django.core.exceptions import MultipleObjectsReturned - from social_auth.utils import setting from social_auth.models import User from social_auth.backends.pipeline import warn_setting @@ -18,7 +16,7 @@ def associate_by_email(details, *args, **kwargs): # objects are returned try: return {'user': User.objects.get(email=email)} - except MultipleObjectsReturned: + except User.MultipleObjectsReturned: raise AuthException(kwargs['backend'], 'Not unique email address.') except User.DoesNotExist: pass diff --git a/social_auth/backends/pipeline/social.py b/social_auth/backends/pipeline/social.py index dc7d30b..0920009 100644 --- a/social_auth/backends/pipeline/social.py +++ b/social_auth/backends/pipeline/social.py @@ -14,7 +14,7 @@ def social_auth_user(backend, uid, user=None, *args, **kwargs): Raise AuthException if UserSocialAuth entry belongs to another user. """ try: - social_user = UserSocialAuth.objects.select_related('user')\ + social_user = UserSocialAuth.select_related()\ .get(provider=backend.name, uid=uid) except UserSocialAuth.DoesNotExist: @@ -37,6 +37,8 @@ def associate_user(backend, user, uid, social_user=None, *args, **kwargs): return None try: + if type(uid) is not str: + uid = str(uid) social = UserSocialAuth.objects.create(user=user, uid=uid, provider=backend.name) except IntegrityError: diff --git a/social_auth/backends/pipeline/user.py b/social_auth/backends/pipeline/user.py index 6cd3676..ea06079 100644 --- a/social_auth/backends/pipeline/user.py +++ b/social_auth/backends/pipeline/user.py @@ -1,7 +1,8 @@ from uuid import uuid4 from social_auth.utils import setting -from social_auth.models import User +from social_auth.models import create_user as create_user_in_db +from social_auth.models import simple_user_exists from social_auth.backends.pipeline import USERNAME, USERNAME_MAX_LENGTH, \ warn_setting from social_auth.signals import socialauth_not_registered, \ @@ -9,12 +10,6 @@ from social_auth.signals import socialauth_not_registered, \ pre_update -def simple_user_exists(*args, **kwargs): - """Return True/False if a User instance exists with the given arguments. - Arguments are directly passed to filter() manager method.""" - return User.objects.filter(*args, **kwargs).exists() - - def get_username(details, user=None, user_exists=simple_user_exists, *args, **kwargs): """Return an username for new user. Return current user username @@ -73,9 +68,9 @@ def create_user(backend, details, response, uid, username, user=None, *args, details=details) return None - email = details.get('email') + email = details.get('email') or None return { - 'user': User.objects.create_user(username=username, email=email), + 'user': create_user_in_db(username=username, email=email), 'is_new': True } diff --git a/social_auth/conf.py b/social_auth/conf.py new file mode 100644 index 0000000..a3f5d88 --- /dev/null +++ b/social_auth/conf.py @@ -0,0 +1,16 @@ +"""Centralized definition of settings""" + + +from django.utils import importlib +from social_auth.utils import setting + + +SOCIAL_AUTH_MODELS = setting('SOCIAL_AUTH_MODELS', 'social_auth.django_models') + + +def get_models_module(): + """Load and return the module specified by the SOCIAL_AUTH_MODELS config + setting. + + """ + return importlib.import_module(SOCIAL_AUTH_MODELS) diff --git a/social_auth/context_processors.py b/social_auth/context_processors.py index 0b1d918..e7d5531 100644 --- a/social_auth/context_processors.py +++ b/social_auth/context_processors.py @@ -1,6 +1,7 @@ from social_auth.backends import get_backends from social_auth.utils import group_backend_by_type from social_auth.models import User +from social_auth.models import get_social_auth_for_user # Note: social_auth_backends, social_auth_by_type_backends and @@ -40,7 +41,7 @@ def social_auth_by_name_backends(request): if isinstance(user, User) and user.is_authenticated(): accounts.update((assoc.provider.replace('-', '_'), assoc) - for assoc in user.social_auth.all()) + for assoc in get_social_auth_for_user(user)) return {'social_auth': accounts} @@ -65,7 +66,7 @@ def backends_data(user): # user comes from request.user usually, on /admin/ it will be an instance # of auth.User and this code will fail if a custom User model was defined if isinstance(user, User) and user.is_authenticated(): - associated = user.social_auth.all() + associated = get_social_auth_for_user(user) not_associated = list(set(available) - set(assoc.provider for assoc in associated)) values['associated'] = associated diff --git a/social_auth/django_models.py b/social_auth/django_models.py index 2a1cfa3..1fce960 100644 --- a/social_auth/django_models.py +++ b/social_auth/django_models.py @@ -1,4 +1,4 @@ -"""Social auth models""" +"""Django ORM models for Social Auth""" from datetime import timedelta from django.db import models @@ -7,6 +7,9 @@ from social_auth.fields import JSONField from social_auth.utils import setting +NAME = 'django_models' + + # If User class is overridden, it *must* provide the following fields # and methods work with django-social-auth: # @@ -22,6 +25,30 @@ else: from django.contrib.auth.models import User +# TODO make this a complementary config setting to SOCIAL_AUTH_USER_MODEL +USERNAME = 'username' + + +def get_username_max_length(): + """Get the max length constraint from the User model username field. + """ + return User._meta.get_field(USERNAME).max_length + + +def simple_user_exists(*args, **kwargs): + """Return True/False if a User instance exists with the given arguments. + Arguments are directly passed to filter() manager method.""" + return User.objects.filter(*args, **kwargs).exists() + + +def create_user(*args, **kwargs): + return User.objects.create_user(*args, **kwargs) + + +def get_social_auth_for_user(user): + return user.social_auth.all() + + class UserSocialAuth(models.Model): """Social Auth association model""" user = models.ForeignKey(User, related_name='social_auth') @@ -62,6 +89,10 @@ class UserSocialAuth(models.Model): pass return None + @classmethod + def select_related(cls): + return cls.objects.select_related('user') + class Nonce(models.Model): """One use numbers""" diff --git a/social_auth/models.py b/social_auth/models.py index 75a66cf..7fe0ecf 100644 --- a/social_auth/models.py +++ b/social_auth/models.py @@ -2,13 +2,10 @@ # TODO define protocol for implementing modules... -from django.conf import settings -from django.utils import importlib +from social_auth import conf -models_module_name = getattr(settings, 'SOCIAL_AUTH_MODELS', - 'social_auth.django_models') -models_module = importlib.import_module(models_module_name) +models_module = conf.get_models_module() this_module = globals() for key in dir(models_module): diff --git a/social_auth/mongoengine_models.py b/social_auth/mongoengine_models.py new file mode 100644 index 0000000..d6b746d --- /dev/null +++ b/social_auth/mongoengine_models.py @@ -0,0 +1,122 @@ +"""MongoEngine models for Social Auth + +Requires MongoEngine 0.6.10 + +""" +# TODO extract common code into base objects/mixins + + +from datetime import timedelta +from mongoengine import DictField +from mongoengine import Document +from mongoengine import IntField +from mongoengine import ReferenceField +from mongoengine import StringField +from social_auth.utils import setting + + +NAME = 'mongoengine_models' + + +# If User class is overridden, it *must* provide the following fields +# and methods work with django-social-auth: +# +# username = CharField() +# last_login = DateTimeField() +# is_active = BooleanField() +# def is_authenticated(): +# ... + +if setting('SOCIAL_AUTH_USER_MODEL'): + User = models.get_model(*setting('SOCIAL_AUTH_USER_MODEL').rsplit('.', 1)) +else: + from mongoengine.django.auth import User + + +# TODO make this a complementary config setting to SOCIAL_AUTH_USER_MODEL +USERNAME = 'username' + + +def get_username_max_length(): + """Get the max length constraint from the User model username field. + """ + return getattr(User, USERNAME).max_length + + +def simple_user_exists(*args, **kwargs): + """Return True/False if a User instance exists with the given arguments. + Arguments are directly passed to filter() manager method.""" + return User.objects.filter(*args, **kwargs).count() + + +def create_user(*args, **kwargs): + return User.objects.create(*args, **kwargs) + + +def get_social_auth_for_user(user): + return UserSocialAuth.objects(user=user) + + +class UserSocialAuth(Document): + """Social Auth association model""" + user = ReferenceField(User) + provider = StringField(max_length=32) + uid = StringField(max_length=255, unique_with='provider') + extra_data = DictField() + + def __unicode__(self): + """Return associated user unicode representation""" + return u'%s - %s' % (unicode(self.user), self.provider) + + @classmethod + def select_related(cls): + return cls.objects #.select_related() No 'user', only provie a depth parameter + + @property + def tokens(self): + """Return access_token stored in extra_data or None""" + # Make import here to avoid recursive imports :-/ + from social_auth.backends import get_backends + backend = get_backends().get(self.provider) + if backend: + return backend.AUTH_BACKEND.tokens(self) + else: + return {} + + def expiration_delta(self): + """Return saved session expiration seconds if any. Is returned in + the form of a timedelta data type. None is returned if there's no + value stored or it's malformed. + """ + if self.extra_data: + name = setting('SOCIAL_AUTH_EXPIRATION', 'expires') + try: + return timedelta(seconds=int(self.extra_data.get(name))) + except (ValueError, TypeError): + pass + return None + + +class Nonce(Document): + """One use numbers""" + server_url = StringField(max_length=255) + timestamp = IntField() + salt = StringField(max_length=40) + + def __unicode__(self): + """Unicode representation""" + return self.server_url + + +class Association(Document): + """OpenId account association""" + server_url = StringField(max_length=255) + handle = StringField(max_length=255) + secret = StringField(max_length=255) # Stored base64 encoded + issued = IntField() + lifetime = IntField() + assoc_type = StringField(max_length=64) + + def __unicode__(self): + """Unicode representation""" + return '%s %s' % (self.handle, self.issued)