Commits

Peter Sagerson committed bb099f4

Add custom signals for populating users and profiles.

Comments (0)

Files changed (7)

django_auth_ldap/backend.py

 from django.contrib.auth.models import User, Group, SiteProfileNotAvailable
 from django.core.cache import cache
 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
+import django.dispatch
 
 from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPGroupType
 
 logger = _LDAPConfig.get_logger()
 
 
+# Signals for populating user objects.
+populate_user = django.dispatch.Signal(providing_args=["user", "ldap_user"])
+populate_user_profile = django.dispatch.Signal(providing_args=["profile", "ldap_user"])
+
+
 class LDAPBackend(object):
     """
     The main backend class. This implements the auth backend API, although it
         
         username = self.backend.ldap_to_django_username(self._username)
 
-        (self._user, created) = self.backend.get_or_create_user(username, self)
+        self._user, created = self.backend.get_or_create_user(username, self)
+        self._user.ldap_user = self
+        self._user.ldap_username = self._username
+
+        should_populate = force_populate or ldap_settings.AUTH_LDAP_ALWAYS_UPDATE_USER or created
 
         if created:
             logger.debug("Created Django user %s", username)
             self._user.set_unusable_password()
             save_user = True
 
-        if(force_populate or ldap_settings.AUTH_LDAP_ALWAYS_UPDATE_USER or created):
+        if should_populate:
             logger.debug("Populating Django user %s", username)
             self._populate_user()
-            self._populate_and_save_user_profile()
             save_user = True
 
         if ldap_settings.AUTH_LDAP_MIRROR_GROUPS:
             self._mirror_groups()
 
+        # Give the client a chance to finish populating the user just before
+        # saving.
+        if should_populate:
+            signal_responses = populate_user.send(self.backend.__class__, user=self._user, ldap_user=self)
+            if len(signal_responses) > 0:
+                save_user = True
+
         if save_user:
             self._user.save()
 
-        self._user.ldap_user = self
-        self._user.ldap_username = self._username
+        # We populate the profile after the user model is saved to give the
+        # client a chance to create the profile.
+        if should_populate:
+            logger.debug("Populating Django user profile for %s", username)
+            self._populate_and_save_user_profile()
 
     def _populate_user(self):
         """
         """
         try:
             profile = self._user.get_profile()
+            save_profile = False
 
             for field, attr in ldap_settings.AUTH_LDAP_PROFILE_ATTR_MAP.iteritems():
                 try:
                     # user_attrs is a hash of lists of attribute values
                     setattr(profile, field, self.attrs[attr][0])
+                    save_profile = True
                 except (KeyError, IndexError):
                     pass
 
-            if len(ldap_settings.AUTH_LDAP_PROFILE_ATTR_MAP) > 0:
+            signal_responses = populate_user_profile.send(self.backend.__class__, profile=profile, ldap_user=self)
+            if len(signal_responses) > 0:
+                save_profile = True
+
+            if save_profile:
                 profile.save()
         except (SiteProfileNotAvailable, ObjectDoesNotExist):
             pass
-    
+
     def _mirror_groups(self):
         """
         Mirrors the user's LDAP groups in the Django database and updates the

django_auth_ldap/models.py

-# This is only here so that this looks like an app for the purpose of unit
-# testing.
+from django.db import models
+
+
+class TestProfile(models.Model):
+    """
+    A user profile model for use by unit tests. This has nothing to do with the
+    authentication backend itself.
+    """
+    user = models.OneToOneField('auth.User')
+    populated = models.BooleanField(default=False)

django_auth_ldap/tests.py

 import sys
 import logging
 
+from django.conf import settings
+import django.db.models.signals
 from django.contrib.auth.models import User, Permission, Group
 from django.test import TestCase
 
+import django_auth_ldap.models
 from django_auth_ldap import backend
 from django_auth_ldap.config import _LDAPConfig, LDAPSearch
 from django_auth_ldap.config import PosixGroupType, MemberDNGroupType, NestedMemberDNGroupType
         self.assertEqual(user.first_name, 'Alice')
         self.assertEqual(user.last_name, 'Adams')
 
+    def test_signal_populate_user(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
+        )
+        def handle_populate_user(sender, **kwargs):
+            self.assert_('user' in kwargs and 'ldap_user' in kwargs)
+            kwargs['user'].populate_user_handled = True
+        backend.populate_user.connect(handle_populate_user)
+
+        user = self.backend.authenticate(username='alice', password='password')
+
+        self.assert_(user.populate_user_handled)
+
+    def test_signal_populate_user_profile(self):
+        settings.AUTH_PROFILE_MODULE = 'django_auth_ldap.TestProfile'
+
+        self._init_settings(
+            AUTH_LDAP_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'])
+
+        def handle_populate_user_profile(sender, **kwargs):
+            self.assert_('profile' in kwargs and 'ldap_user' in kwargs)
+            kwargs['profile'].populated = True
+
+        django.db.models.signals.post_save.connect(handle_user_saved, sender=User)
+        backend.populate_user_profile.connect(handle_populate_user_profile)
+
+        user = self.backend.authenticate(username='alice', password='password')
+
+        self.assert_(user.get_profile().populated)
+
     def test_no_update_existing(self):
         self._init_settings(
             AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
 string that can not be successfully decoded will be left as-is; this may apply
 to binary values such as Active Directory's objectSid.
 
+If you would like to perform any additional population of user or profile
+objects, django_auth_ldap exposes two custom signals to help:
+:data:`~django_auth_ldap.backend.populate_user` and
+:data:`~django_auth_ldap.backend.populate_user_profile`. These are sent after
+the backend has finished populating the respective objects and before they are
+saved to the database. You can use this to propagate additional information from
+the LDAP directory to the user and profile objects any way you like.
+
 .. note::
 
     Users created by :class:`~django_auth_ldap.backend.LDAPBackend` will have an
 
 .. module:: django_auth_ldap.backend
 
+.. data:: populate_user
+
+    This is a Django signal that is sent when clients should perform additional
+    customization of a :class:`~django.contrib.auth.models.User` object. It is
+    sent after a user has been authenticated and the backend has finished
+    populating it, and just before it is saved. The client may take this
+    opportunity to populate additional model fields, perhaps based on
+    ``ldap_user.attrs``. This signal has two keyword arguments: ``user`` is the
+    :class:`~django.contrib.auth.models.User` object and ``ldap_user`` is the
+    same as ``user.ldap_user``. The sender is the
+    :class:`~django_auth_ldap.backend.LDAPBackend` class.
+
+.. data:: populate_user_profile
+
+    Like :data:`~django_auth_ldap.backend.populate_user`, but sent for the user
+    profile object. This will only be sent if the user has an existing profile.
+    As with :data:`~django_auth_ldap.backend.populate_user`, it is sent after the
+    backend has finished setting properties and before the object is saved. This
+    signal has two keyword arguments: ``profile`` is the user profile object and
+    ``ldap_user`` is the same as ``user.ldap_user``. The sender is the
+    :class:`~django_auth_ldap.backend.LDAPBackend` class.
+
 .. class:: LDAPBackend
 
     :class:`~django_auth_ldap.backend.LDAPBackend` has one method that may be

tests/__init__.py

Empty file removed.

tests/runtests.py

-#!/usr/bin/env python
-
-import sys
-import os
-from optparse import OptionParser
-
-from django.core.management import setup_environ
-
-try:
-    import settings # Assumed to be in the same directory.
-except ImportError:
-    import sys
-    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r.\n" % __file__)
-    sys.exit(1)
-
-
-options, args = ({}, [])
-
-def main():
-    parse_options()
-    setup()
-    run()
-
-def setup():
-    tests_path = setup_environ(settings)
-    sys.path.append(os.path.dirname(tests_path))
-
-def parse_options():
-    global options, args
-    
-    parser = OptionParser("Usage: %prog [options] [test test test ...]")
-    parser.add_option("-v", "--verbosity", action="store", dest="verbosity", type="int", default=0,
-        help="Verbosity level; 0=minimal output, 1=normal output, 2=all output")
-    parser.add_option("--noinput", action="store_false", dest="interactive", default=True,
-        help="Tells Django to NOT prompt the user for input of any kind.")
-    
-    (options, args) = parser.parse_args()
-
-def run():
-    from django.test.utils import get_runner
-    import django.conf
-
-    test_runner = get_runner(django.conf.settings)
-    tests = ['django_auth_ldap.' + arg for arg in args]
-    if len(tests) == 0:
-        tests = ['django_auth_ldap']
-
-    test_runner(tests, verbosity=options.verbosity, interactive=options.interactive)    
-
-
-if __name__ == '__main__':
-    main()

tests/settings.py

-DATABASE_ENGINE = 'sqlite3'
-ROOT_URLCONF = None
-
-INSTALLED_APPS = [
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django_auth_ldap',
-]