Permalink
Browse files

first commit

  • Loading branch information...
0 parents commit c60a82e150382c1e1ac4092ed034a3ea0de5eab7 @winjer winjer committed Jan 8, 2012
No changes.
No changes.
@@ -0,0 +1,15 @@
+""" LDAP Pixiedust
+
+Sprinkles magic pixie dust over your Django/LDAP integration.
+
+In particular it provides:
+
+ * A backend that can authenticate by e-mail, but otherwise play nicely with
+ * standard Django a set of hooks that will magically synchronise changes to users, groups and profiles back to your LDAP database
+
+"""
+
+from .backend import EmailLoginBackend
+from .ldap import LDAPConnectionMixin
+from . import sync
+
@@ -0,0 +1,86 @@
+
+from __future__ import absolute_import
+
+from django.contrib.auth import models as auth_models
+from django.contrib.auth.models import User, Group
+from django.utils.encoding import smart_str
+from django.utils.hashcompat import sha_constructor, md5_constructor
+from django_auth_ldap.backend import LDAPBackend, _LDAPUser
+from collections import defaultdict
+from django.db import models
+
+import uuid
+import logging
+
+from .user import SynchronisingUserAdapter
+from .utils import django_password_to_ldap
+from .ldap import LDAPConnectionMixin
+from . import settings
+
+logger = logging.getLogger('django_ldap_pixiedust')
+
+class DecoupledUserClassMixin(object):
+ """ Make it easy to use a different _LDAPUser class from within the backend. """
+ userclass = _LDAPUser
+ def authenticate(self, username, password):
+ ldap_user = self.userclass(self, username=username)
+ user = ldap_user.authenticate(password)
+ return user
+
+ def get_user(self, user_id):
+ user = None
+ try:
+ user = User.objects.get(pk=user_id)
+ self.userclass(self, user=user) # sets user.ldap_user
+ except User.DoesNotExist:
+ pass
+ return user
+
+if settings.ldap_settings.LDAP_PIXIEDUST_COMPATIBLE_PASSWORDS:
+ # monkeypatch django.contrib.auth to alter the digest mechanism
+ # look away now
+ def get_hexdigest(algorithm, salt, raw_password):
+
+ """ replacement hashing mechanism that hashes password first, instead
+ of salt first. This means that generated passwords are valid for LDAP
+ as well as Django.
+
+ Only supports SHA1. """
+
+ raw_password, salt = smart_str(raw_password), smart_str(salt)
+ if algorithm == 'crypt':
+ try:
+ import crypt
+ except ImportError:
+ raise ValueError('"crypt" password algorithm not supported in this environment')
+ return crypt.crypt(raw_password, salt)
+
+ if algorithm == 'md5':
+ return md5_constructor(salt + raw_password).hexdigest()
+ elif algorithm == 'sha1':
+ # in django by default this is salt + raw_password
+ return sha_constructor(raw_password + salt).hexdigest()
+ raise ValueError("Got unknown password algorithm type in password.")
+ auth_models.get_hexdigest = get_hexdigest
+
+class EmailLoginBackend(LDAPConnectionMixin, LDAPBackend):
+
+ """ Locates the user by email address, even though their dn is determined by a unique username. """
+
+ def authenticate(self, username, password):
+ """ This actually takes the email address as the username, but
+ assembles a user based on the correct DN by searching first using the
+ login search configuration, then delegating user creation back to the
+ usual mechanism. """
+
+ search = settings.ldap_settings.LDAP_PIXIEDUST_LOGIN_SEARCH
+ results = search.execute(self._get_connection(), {"user": username})
+ if results is not None and len(results) == 1:
+ id_attr = settings.ldap_settings.LDAP_PIXIEDUST_USERNAME_DN_ATTRIBUTE
+ user_dn, user_attrs = results[0]
+ if id_attr in user_attrs:
+ real_username = user_attrs[id_attr][0]
+ return super(EmailLoginBackend, self).authenticate(real_username, password)
+ else:
+ raise ValueError("LDAP User %r does not have username attribute %r" % (user_dn, id_attr))
+
@@ -0,0 +1,57 @@
+
+from __future__ import absolute_import
+
+from . import settings
+import ldap
+
+class LDAPConnectionMixin(object):
+
+ """ Based on equivalent code in django-auth-ldap. """
+
+ def _bind(self):
+ """
+ Binds to the LDAP server with AUTH_LDAP_BIND_DN and
+ AUTH_LDAP_BIND_PASSWORD.
+ """
+ self._bind_as(settings.ldap_settings.AUTH_LDAP_BIND_DN,
+ settings.ldap_settings.AUTH_LDAP_BIND_PASSWORD)
+
+ self._connection_bound = True
+
+ def _bind_as(self, bind_dn, bind_password):
+ """
+ Binds to the LDAP server with the given credentials. This does not trap
+ exceptions.
+
+ If successful, we set self._connection_bound to False under the
+ assumption that we're not binding as the default user. Callers can set
+ it to True as appropriate.
+ """
+ self._get_connection().simple_bind_s(bind_dn.encode('utf-8'),
+ bind_password.encode('utf-8'))
+
+ self._connection_bound = False
+
+ # TODO: this is pretty fugly. a bunch of this stuff should be in
+ # __init__ really, but I've delayed refactoring to stop this getting
+ # confused with the historical connection stuff from django_auth_ldap
+ def _get_connection(self):
+ """
+ Returns our cached LDAPObject, which may or may not be bound.
+ """
+ if not hasattr(self, 'ldap'):
+ self.ldap = ldap
+ if not hasattr(self, '_connection'):
+ self._connection = None
+ if self._connection is None:
+ self._connection = self.ldap.initialize(settings.ldap_settings.AUTH_LDAP_SERVER_URI)
+
+ for opt, value in settings.ldap_settings.AUTH_LDAP_CONNECTION_OPTIONS.iteritems():
+ self._connection.set_option(opt, value)
+
+ if settings.ldap_settings.AUTH_LDAP_START_TLS:
+ #logger.debug("Initiating TLS")
+ self._connection.start_tls_s()
+
+ return self._connection
+
No changes.
@@ -0,0 +1,33 @@
+
+""" Synchronise the Django user, profile and group tables against LDAP. """
+
+from django.core.management.base import BaseCommand
+from optparse import make_option
+import ldap
+from django_ldap_pixiedust import settings
+from django_ldap_pixiedust.ldap import LDAPConnectionMixin
+from django_ldap_pixiedust.user import SynchronisingUserAdapter
+from django_auth_ldap.backend import LDAPBackend, _LDAPUser
+from django.contrib.auth.models import User
+from django_ldap_pixiedust import sync
+
+class Command(BaseCommand):
+
+ help = "example help"
+
+ option_list = BaseCommand.option_list + (
+ make_option("--verbose", action="store_true", default=False),
+ )
+
+ def handle(self, *args, **options):
+ for arg in args:
+ handler = getattr(self, "handle_" + arg)
+ handler(**options)
+
+ def handle_fromldap(self, **options):
+ synchroniser = sync.LDAPSynchroniser()
+ synchroniser.synchronise_from_ldap()
+
+ def handle_toldap(self, **options):
+ synchroniser = sync.LDAPSynchroniser()
+ synchroniser.synchronise_to_ldap()
@@ -0,0 +1,12 @@
+from django.db import models
+
+
+class PixieDustTestProfile(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')
+ location = models.CharField(max_length=100)
+ is_special = models.BooleanField(default=False)
+ populated = models.BooleanField(default=False)
@@ -0,0 +1,24 @@
+from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings
+
+class LDAPSettings(BaseLDAPSettings):
+ pass
+
+LDAPSettings.defaults.update({
+ 'LDAP_PIXIEDUST_SYNC_USER': False,
+ 'LDAP_PIXIEDUST_SYNC_PROFILE': False,
+ 'LDAP_PIXIEDUST_SYNC_GROUPS': False,
+ 'LDAP_PIXIEDUST_LOGIN_SEARCH': None,
+ 'LDAP_PIXIEDUST_GROUP_DN_TEMPLATE': '',
+ 'LDAP_PIXIEDUST_COMPATIBLE_PASSWORDS': False,
+ 'LDAP_PIXIEDUST_USER_OBJECTCLASSES': [
+ 'organizationalPerson',
+ 'inetOrgPerson',
+ ],
+ 'LDAP_PIXIEDUST_USERNAME_DN_ATTRIBUTE': 'uid',
+ 'LDAP_PIXIEDUST_DEFAULT_ATTR_MAP': {},
+ 'AUTH_PROFILE_MODULE': '',
+ 'LDAP_PIXIEDUST_ALL_USERS': None,
+ 'LDAP_PIXIEDUST_GROUP_OBJECTCLASS': 'groupOfNames',
+ })
+
+ldap_settings = LDAPSettings()
@@ -0,0 +1,110 @@
+from django.contrib.auth.models import Group, User
+from django_auth_ldap.backend import LDAPBackend, _LDAPUser
+from django.db.models.signals import post_save, post_delete, m2m_changed
+
+from . import backend, ldap, settings
+from .user import SynchronisingUserAdapter
+
+import logging
+
+logger = logging.getLogger("django_ldap_pixiedust")
+
+def group_sync_handler(sender, instance, action, pk_set, **kwargs):
+ if sender == User.groups.through:
+ user = LDAPBackend().get_user(instance.id)
+ sync = SynchronisingUserAdapter(user)
+ if pk_set is not None:
+ groups = [Group.objects.get(pk=x) for x in pk_set]
+ if action == 'post_clear':
+ sync.clear_groups()
+ elif action == 'post_add':
+ sync.group_add(groups)
+ elif action == 'post_remove':
+ sync.group_remove(groups)
+
+def profile_sync_handler(sender, instance, created, **kwargs):
+ from django.db import models
+ app_label, model_name = settings.ldap_settings.AUTH_PROFILE_MODULE.split('.')
+ profile_model = models.get_model(app_label, model_name)
+
+ # we're getting the octopus profile, not the pixiedust profile
+ if sender == profile_model:
+ user = LDAPBackend().get_user(instance.id)
+ sync = SynchronisingUserAdapter(user)
+ sync.synchronise_profile(created)
+
+def user_sync_handler(sender, **kwargs):
+ instance = kwargs.pop('instance', None)
+ created = kwargs.pop('created', None)
+ backend = LDAPBackend()
+ if sender == User:
+ user = backend.get_user(instance.id)
+ sync = SynchronisingUserAdapter(user)
+ sync.synchronise(created)
+
+def activate(sync_user, sync_groups, sync_profile):
+ if sync_groups:
+ logger.warning("Group changes will be synchronised to LDAP")
+ m2m_changed.connect(group_sync_handler)
+ else:
+ m2m_changed.disconnect(group_sync_handler)
+
+ if sync_user:
+ logger.warning("User changes will be synchronised to LDAP")
+ post_save.connect(user_sync_handler)
+ else:
+ post_save.disconnect(user_sync_handler)
+
+ if sync_profile:
+ logger.warning("User profile changes will be synchronised to LDAP")
+ post_save.connect(profile_sync_handler)
+ else:
+ post_save.disconnect(profile_sync_handler)
+
+def deactivate():
+ activate(False, False, False)
+
+def activate_fromsettings():
+ activate(settings.ldap_settings.LDAP_PIXIEDUST_SYNC_USER,
+ settings.ldap_settings.LDAP_PIXIEDUST_SYNC_GROUPS,
+ settings.ldap_settings.LDAP_PIXIEDUST_SYNC_PROFILE
+ )
+
+# this is our default
+activate_fromsettings()
+
+class LDAPSynchroniser(ldap.LDAPConnectionMixin):
+
+ """ This provides complete database synchronisation - either from or to
+ the LDAP server. This will synchronise every user and group. It's called
+ by the "ldapsync" management command. """
+
+ def __init__(self):
+ self.conn = self._get_connection()
+ self.backend = LDAPBackend()
+
+ def ldap_users(self):
+ search = settings.ldap_settings.LDAP_PIXIEDUST_ALL_USERS
+ for dn, attrs in search.execute(self.conn):
+ user_id = attrs[settings.ldap_settings.LDAP_PIXIEDUST_USERNAME_DN_ATTRIBUTE][0]
+ yield _LDAPUser(self.backend, username=user_id)
+
+ def model_users(self):
+ return User.objects.all()
+
+ def synchronise_from_ldap(self):
+ deactivate()
+ for user in self.ldap_users():
+ logger.debug("Synchronising %r" % repr(user))
+ user._get_or_create_user()
+ activate_fromsettings()
+
+ def synchronise_to_ldap(self):
+ for user in self.model_users():
+ ldap_user = self.backend.get_user(user.id)
+ sync = SynchronisingUserAdapter(ldap_user)
+ logger.debug("Synchronising %r" % repr(user))
+ sync.synchronise_groups() # must happen first, because it clears groups!
+ sync.synchronise()
+ sync.synchronise_profile()
+
@@ -0,0 +1,3 @@
+from test_user import *
+from test_backend import *
+from test_sync import *
Oops, something went wrong.

0 comments on commit c60a82e

Please sign in to comment.