Commits

Peter Sagerson committed 348b921

Support Django 1.5's custom user models.

Comments (0)

Files changed (7)

django_auth_ldap/backend.py

 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
 import django.dispatch
 
+# Support Django 1.5's custom user models
+try:
+    from django.contrib.auth import get_user_model
+    get_user_username = lambda u: u.get_username()
+except ImportError:
+    get_user_model = lambda: User
+    get_user_username = lambda u: u.username
+
+
 from django_auth_ldap.config import _LDAPConfig, LDAPSearch
 
 
         user = None
 
         try:
-            user = User.objects.get(pk=user_id)
+            user = get_user_model().objects.get(pk=user_id)
             _LDAPUser(self, user=user) # This sets user.ldap_user
-        except User.DoesNotExist:
+        except ObjectDoesNotExist:
             pass
 
         return user
         username is the Django-friendly username of the user. ldap_user.dn is
         the user's DN and ldap_user.attrs contains all of their LDAP attributes.
         """
-        return User.objects.get_or_create(username__iexact=username, defaults={'username': username.lower()})
+        model = get_user_model()
+        username_field = getattr(model, 'USERNAME_FIELD', 'username')
+
+        kwargs = {
+            username_field + '__iexact': username,
+            'defaults': {username_field: username.lower()}
+        }
+
+        return model.objects.get_or_create(**kwargs)
 
     def ldap_to_django_username(self, username):
         return username
 
     def _set_authenticated_user(self, user):
         self._user = user
-        self._username = self.backend.django_to_ldap_username(user.username)
+        self._username = self.backend.django_to_ldap_username(get_user_username(user))
 
         user.ldap_user = self
         user.ldap_username = self._username
             self._user.save()
 
         # We populate the profile after the user model is saved to give the
-        # client a chance to create the profile.
-        if should_populate:
+        # client a chance to create the profile. Custom user models in Django
+        # 1.5 probably won't have a get_profile method.
+        if should_populate and hasattr(self._user, 'get_profile'):
             self._populate_and_save_user_profile()
 
     def _populate_user(self):
             profile = self._user.get_profile()
             save_profile = False
 
-            logger.debug("Populating Django user profile for %s", self._user.username)
+            logger.debug("Populating Django user profile for %s", get_user_username(self._user))
 
             save_profile = self._populate_profile_from_attributes(profile) or save_profile
             save_profile = self._populate_profile_from_group_memberships(profile) or save_profile
             if save_profile:
                 profile.save()
         except (SiteProfileNotAvailable, ObjectDoesNotExist):
-            logger.debug("Django user %s does not have a profile to populate", self._user.username)
+            logger.debug("Django user %s does not have a profile to populate", get_user_username(self._user))
 
     def _populate_profile_from_attributes(self, profile):
         """

django_auth_ldap/models.py

 from django.db import models
 
 
+# Support for testing Django 1.5's custom user models.
+try:
+    from django.contrib.auth.models import AbstractBaseUser
+except ImportError:
+    from django.contrib.auth.models import User
+
+    TestUser = User
+else:
+    class TestUser(AbstractBaseUser):
+        identifier = models.CharField(max_length=40, unique=True, db_index=True)
+
+        USERNAME_FIELD = 'identifier'
+
+        def get_full_name(self):
+            return self.identifier
+
+        def get_short_name(self):
+            return self.identifier
+
+
 class TestProfile(models.Model):
     """
     A user profile model for use by unit tests. This has nothing to do with the

django_auth_ldap/tests.py

     from sets import Set as set     # Python 2.3 fallback
 
 from collections import defaultdict
+from copy import deepcopy
 import logging
 import pickle
 import sys
 from django.contrib.auth.models import User, Permission, Group
 from django.test import TestCase
 
-import django_auth_ldap.models
+try:
+    from django.test.utils import override_settings
+except ImportError:
+    override_settings = lambda *args, **kwargs: (lambda v: v)
+
+from django_auth_ldap.models import TestUser, TestProfile
 from django_auth_ldap import backend
 from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
 from django_auth_ldap.config import PosixGroupType, MemberDNGroupType, NestedMemberDNGroupType
         self.assertEqual(self.mock_ldap.ldap_methods_called(),
             ['initialize', 'simple_bind_s'])
 
+    def test_deepcopy(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
+        )
+
+        user = self.backend.authenticate(username='Alice', password='password')
+        user = deepcopy(user)
+
+    @override_settings(AUTH_USER_MODEL='django_auth_ldap.TestUser')
+    def test_auth_custom_user(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+        )
+
+        user = self.backend.authenticate(username='Alice', password='password')
+
+        self.assert_(isinstance(user, TestUser))
+
+    @override_settings(AUTH_USER_MODEL='django_auth_ldap.TestUser')
+    def test_get_custom_user(self):
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+        )
+
+        user = self.backend.authenticate(username='Alice', password='password')
+        user = self.backend.get_user(user.id)
+
+        self.assert_(isinstance(user, TestUser))
+
     def test_new_user_whitespace(self):
         self._init_settings(
             USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
 
         def handle_user_saved(sender, **kwargs):
             if kwargs['created']:
-                django_auth_ldap.models.TestProfile.objects.create(user=kwargs['instance'])
+                TestProfile.objects.create(user=kwargs['instance'])
 
         def handle_populate_user_profile(sender, **kwargs):
             self.assert_('profile' in kwargs and 'ldap_user' in kwargs)
 
         def handle_user_saved(sender, **kwargs):
             if kwargs['created']:
-                django_auth_ldap.models.TestProfile.objects.create(user=kwargs['instance'])
+                TestProfile.objects.create(user=kwargs['instance'])
 
         django.db.models.signals.post_save.connect(handle_user_saved, sender=User)
 

docs/source/authentication.rst

 -------------
 
 If your LDAP server isn't running locally on the default port, you'll want to
-start by setting :setting:`AUTH_LDAP_SERVER_URI` to point to your server.
+start by setting :setting:`AUTH_LDAP_SERVER_URI` to point to your server. The
+value of this setting can be anything that your LDAP library supports. For
+instance, openldap may allow you to give a comma- or space-separated list of
+URIs to try in sequence.
 
 .. code-block:: python
 

docs/source/install.rst

 
 This authentication backend enables a Django project to authenticate against any
 LDAP server. To use it, add :class:`django_auth_ldap.backend.LDAPBackend` to
-:django:setting:`AUTHENTICATION_BACKENDS`. It is not necessary to add
-`django_auth_ldap` to :django:setting:`INSTALLED_APPS` unless you would like to
-run the unit tests. LDAP configuration can be as simple as a single
-distinguished name template, but there are many rich options for working with
+:django:setting:`AUTHENTICATION_BACKENDS`. Adding `django_auth_ldap` to
+:django:setting:`INSTALLED_APPS` is not recommended unless you would like to run
+the unit tests. LDAP configuration can be as simple as a single distinguished
+name template, but there are many rich options for working with
 :class:`~django.contrib.auth.models.User` objects, groups, and permissions. This
 backend depends on the `python-ldap <http://www.python-ldap.org/>`_ module.
 

docs/source/reference.rst

 
     .. method:: get_or_create_user(self, username, ldap_user)
 
-        Given a username and an LDAP user object, this must return the
-        associated Django User object. The ``username`` argument has already
-        been passed through
+        Given a username and an LDAP user object, this must return a valid
+        Django user model instance. The ``username`` argument has already been
+        passed through
         :meth:`~django_auth_ldap.backend.LDAPBackend.ldap_to_django_username`.
         You can get information about the LDAP user via ``ldap_user.dn`` and
         ``ldap_user.attrs``. The return value must be the same as
-        ``User.objects.get_or_create()``: a (User, created) two-tuple.
+        :meth:`~django.db.models.query.QuerySet.get_or_create`: an (instance,
+        created) two-tuple.
 
-        The default implementation calls ``User.objects.get_or_create()``, using
-        a case-insensitive query and creating new users with lowercase
-        usernames. Subclasses are welcome to associate LDAP users to Django
-        users any way they like.
+        The default implementation calls ``<model>.objects.get_or_create()``,
+        using a case-insensitive query and creating new users with lowercase
+        usernames. In Django 1.5, custom user models will be respected; in
+        earlier versions, the model is always
+        :class:`django.contrib.auth.models.User`. Subclasses are welcome to
+        associate LDAP users to Django users any way they like.
 
     .. method:: ldap_to_django_username(username)
 

docs/source/users.rst

 ============
 
 Authenticating against an external source is swell, but Django's auth module is
-tightly bound to the :class:`django.contrib.auth.models.User` model. Thus, when
-a user logs in, we have to create a :class:`~django.contrib.auth.models.User`
+tightly bound to a user model. When a user logs in, we have to create a model
 object to represent them in the database. Because the LDAP search is
 case-insensitive, the default implementation also searches for existing Django
 users with an iexact query and new users are created with lowercase usernames.
 See :meth:`~django_auth_ldap.backend.LDAPBackend.get_or_create_user` if you'd
 like to override this behavior.
 
+.. note::
+
+    Prior to Django 1.5, user objects were always instances of
+    :class:`~django.contrib.auth.models.User`. Current versions of Django
+    support custom user models via the :setting:`AUTH_USER_MODEL` setting. As of
+    version 1.1.4, django-auth-ldap will respect custom user models.
+
 The only required field for a user is the username, which we obviously have. The
 :class:`~django.contrib.auth.models.User` model is picky about the characters
 allowed in usernames, so :class:`~django_auth_ldap.backend.LDAPBackend` includes
 :class:`~django_auth_ldap.backend.LDAPBackend` to implement these hooks; by
 default the username is not modified. :class:`~django.contrib.auth.models.User`
 objects that are authenticated by :class:`~django_auth_ldap.backend.LDAPBackend`
-will have an :attr:`~django.contrib.auth.models.User.ldap_username` attribute
-with the original (LDAP) username.
-:attr:`~django.contrib.auth.models.User.username` will, of course, be the Django
-username.
+will have an :attr:`ldap_username` attribute with the original (LDAP) username.
+:attr:`~django.contrib.auth.models.User.username` (or
+:meth:`~django.contrib.auth.models.AbstractBaseUser.get_username`) will, of
+course, be the Django username.
 
 .. note::