django-auth-ldap-1.1.8/ 0000755 0000766 0000024 00000000000 12273247061 015434 5 ustar psagers staff 0000000 0000000 django-auth-ldap-1.1.8/CHANGES 0000644 0000766 0000024 00000002731 12273246555 016441 0 ustar psagers staff 0000000 0000000 v1.1.8 - 2014-02-01 ------------------- * Fix `#43`_: Update :class:`~django_auth_ldap.config.LDAPSearchUnion` to work for group searches in addition to user searches. * Tox no longer supports Python 2.5, so our tests now run on 2.6 and 2.7 only. .. _#43: https://bitbucket.org/psagers/django-auth-ldap/issue/43/ v1.1.7 - 2013-11-19 ------------------- * Bug fix: :setting:`AUTH_LDAP_GLOBAL_OPTIONS` could be ignored in some cases (such as :func:`~django_auth_ldap.backend.LDAPBackend.populate_user`). v1.1.5 - 2013-10-25 ------------------- * Fix `#41`_: Support POSIX group permissions with no gidNumber attribute. * Support multiple group DNs for \*_FLAGS_BY_GROUP. .. _#41: https://bitbucket.org/psagers/django-auth-ldap/issue/41/ v1.1.4 - 2013-03-09 ------------------- * Add support for Django 1.5's custom user models. v1.1.3 - 2013-01-05 ------------------- * Fix `#33`_: Reject empty passwords by default. Unless :setting:`AUTH_LDAP_PERMIT_EMPTY_PASSWORD` is set to True, LDAPBackend.authenticate() will immediately return None if the password is empty. This is technically backwards-incompatible, but it's a more secure default for those LDAP servers that are configured such that binds without passwords always succeed. * Fix `#39`_: Add support for pickling LDAP-authenticated users. .. _#33: https://bitbucket.org/psagers/django-auth-ldap/issue/33/ .. _#39: https://bitbucket.org/psagers/django-auth-ldap/issue/39/ .. vim: ft=rst nospell tw=80 django-auth-ldap-1.1.8/django_auth_ldap/ 0000755 0000766 0000024 00000000000 12273247061 020717 5 ustar psagers staff 0000000 0000000 django-auth-ldap-1.1.8/django_auth_ldap/__init__.py 0000644 0000766 0000024 00000000101 12273245455 023025 0 ustar psagers staff 0000000 0000000 version = (1, 1, 8) version_string = '.'.join(map(str, version)) django-auth-ldap-1.1.8/django_auth_ldap/backend.py 0000644 0000766 0000024 00000071742 12242742324 022671 0 ustar psagers staff 0000000 0000000 # Copyright (c) 2009, Peter Sagerson # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # - Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ LDAP authentication backend Complete documentation can be found in docs/howto/auth-ldap.txt (or the thing it compiles to). Use of this backend requires the python-ldap module. To support unit tests, we import ldap in a single centralized place (config._LDAPConfig) so that the test harness can insert a mock object. A few notes on naming conventions. If an identifier ends in _dn, it is a string representation of a distinguished name. If it ends in _info, it is a 2-tuple containing a DN and a dictionary of lists of attributes. ldap.search_s returns a list of such structures. An identifier that ends in _attrs is the dictionary of attributes from the _info structure. A connection is an LDAPObject that has been successfully bound with a DN and password. The identifier 'user' always refers to a User model object; LDAP user information will be user_dn or user_info. Additional classes can be found in the config module next to this one. """ import ldap import sys import traceback import pprint import copy from django.contrib.auth.models import User, Group, Permission, SiteProfileNotAvailable from django.core.cache import cache 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 # noqa get_user_username = lambda u: u.username from django_auth_ldap.config import _LDAPConfig, LDAPSearch 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 actually delegates most of its work to _LDAPUser, which is defined next. """ supports_anonymous_user = False supports_object_permissions = True supports_inactive_user = False _settings = None _ldap = None # The cached ldap module (or mock object) # This is prepended to our internal setting names to produce the names we # expect in Django's settings file. Subclasses can change this in order to # support multiple collections of settings. settings_prefix = 'AUTH_LDAP_' def __getstate__(self): """ Exclude certain cached properties from pickling. """ state = filter( lambda (k, v): k not in ['_settings', '_ldap'], self.__dict__.iteritems() ) return dict(state) def _get_settings(self): if self._settings is None: self._settings = LDAPSettings(self.settings_prefix) return self._settings def _set_settings(self, settings): self._settings = settings settings = property(_get_settings, _set_settings) def _get_ldap(self): if self._ldap is None: from django.conf import settings options = getattr(settings, 'AUTH_LDAP_GLOBAL_OPTIONS', None) self._ldap = _LDAPConfig.get_ldap(options) return self._ldap ldap = property(_get_ldap) def get_user_model(self): """ By default, this will return the model class configured by AUTH_USER_MODEL. Subclasses may wish to override it and return a proxy model. """ return get_user_model() # # The Django auth backend API # def authenticate(self, username, password): if len(password) == 0 and not self.settings.PERMIT_EMPTY_PASSWORD: logger.debug('Rejecting empty password for %s' % username) return None ldap_user = _LDAPUser(self, username=username.strip()) user = ldap_user.authenticate(password) return user def get_user(self, user_id): user = None try: user = self.get_user_model().objects.get(pk=user_id) _LDAPUser(self, user=user) # This sets user.ldap_user except ObjectDoesNotExist: pass return user def has_perm(self, user, perm, obj=None): return perm in self.get_all_permissions(user, obj) def has_module_perms(self, user, app_label): for perm in self.get_all_permissions(user): if perm[:perm.index('.')] == app_label: return True return False def get_all_permissions(self, user, obj=None): return self.get_group_permissions(user, obj) def get_group_permissions(self, user, obj=None): if not hasattr(user, 'ldap_user') and self.settings.AUTHORIZE_ALL_USERS: _LDAPUser(self, user=user) # This sets user.ldap_user if hasattr(user, 'ldap_user'): return user.ldap_user.get_group_permissions() else: return set() # # Bonus API: populate the Django user from LDAP without authenticating. # def populate_user(self, username): ldap_user = _LDAPUser(self, username=username) user = ldap_user.populate_user() return user # # Hooks for subclasses # def get_or_create_user(self, username, ldap_user): """ This must return a (User, created) 2-tuple for the given LDAP 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. """ model = self.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 django_to_ldap_username(self, username): return username class _LDAPUser(object): """ Represents an LDAP user and ultimately fields all requests that the backend receives. This class exists for two reasons. First, it's convenient to have a separate object for each request so that we can use object attributes without running into threading problems. Second, these objects get attached to the User objects, which allows us to cache expensive LDAP information, especially around groups and permissions. self.backend is a reference back to the LDAPBackend instance, which we need to access the ldap module and any hooks that a subclass has overridden. """ class AuthenticationFailed(Exception): pass # Defaults _user = None _user_dn = None _user_attrs = None _groups = None _group_permissions = None _connection = None _connection_bound = False # # Initialization # def __init__(self, backend, username=None, user=None): """ A new LDAPUser must be initialized with either a username or an authenticated User object. If a user is given, the username will be ignored. """ self.backend = backend self._username = username if user is not None: self._set_authenticated_user(user) if username is None and user is None: raise Exception("Internal error: _LDAPUser improperly initialized.") def __deepcopy__(self, memo): obj = object.__new__(self.__class__) obj.backend = self.backend obj._user = copy.deepcopy(self._user, memo) # This is all just cached immutable data. There's no point copying it. obj._username = self._username obj._user_dn = self._user_dn obj._user_attrs = self._user_attrs obj._groups = self._groups obj._group_permissions = self._group_permissions # The connection couldn't be copied even if we wanted to obj._connection = self._connection obj._connection_bound = self._connection_bound return obj def __getstate__(self): """ Most of our properties are cached from the LDAP server. We only want to pickle a few crucial things. """ state = filter( lambda (k, v): k in ['backend', '_username', '_user'], self.__dict__.iteritems() ) return dict(state) def _set_authenticated_user(self, user): self._user = user self._username = self.backend.django_to_ldap_username(get_user_username(user)) user.ldap_user = self user.ldap_username = self._username def _get_ldap(self): return self.backend.ldap ldap = property(_get_ldap) def _get_settings(self): return self.backend.settings settings = property(_get_settings) # # Entry points # def authenticate(self, password): """ Authenticates against the LDAP directory and returns the corresponding User object if successful. Returns None on failure. """ user = None try: self._authenticate_user_dn(password) self._check_requirements() self._get_or_create_user() user = self._user except self.AuthenticationFailed, e: logger.debug(u"Authentication failed for %s" % self._username) except ldap.LDAPError, e: logger.warning(u"Caught LDAPError while authenticating %s: %s", self._username, pprint.pformat(e)) except Exception: logger.exception(u"Caught Exception while authenticating %s", self._username) raise return user def get_group_permissions(self): """ If allowed by the configuration, this returns the set of permissions defined by the user's LDAP group memberships. """ if self._group_permissions is None: self._group_permissions = set() if self.settings.FIND_GROUP_PERMS: try: self._load_group_permissions() except ldap.LDAPError, e: logger.warning("Caught LDAPError loading group permissions: %s", pprint.pformat(e)) return self._group_permissions def populate_user(self): """ Populates the Django user object using the default bind credentials. """ user = None try: # self.attrs will only be non-None if we were able to load this user # from the LDAP directory, so this filters out nonexistent users. if self.attrs is not None: self._get_or_create_user(force_populate=True) user = self._user except ldap.LDAPError, e: logger.warning(u"Caught LDAPError while authenticating %s: %s", self._username, pprint.pformat(e)) except Exception, e: logger.error(u"Caught Exception while authenticating %s: %s", self._username, pprint.pformat(e)) logger.error(''.join(traceback.format_tb(sys.exc_info()[2]))) raise return user # # Public properties (callbacks). These are all lazy for performance reasons. # def _get_user_dn(self): if self._user_dn is None: self._load_user_dn() return self._user_dn dn = property(_get_user_dn) def _get_user_attrs(self): if self._user_attrs is None: self._load_user_attrs() return self._user_attrs attrs = property(_get_user_attrs) def _get_group_dns(self): return self._get_groups().get_group_dns() group_dns = property(_get_group_dns) def _get_group_names(self): return self._get_groups().get_group_names() group_names = property(_get_group_names) def _get_bound_connection(self): if not self._connection_bound: self._bind() return self._get_connection() connection = property(_get_bound_connection) # # Authentication # def _authenticate_user_dn(self, password): """ Binds to the LDAP server with the user's DN and password. Raises AuthenticationFailed on failure. """ if self.dn is None: raise self.AuthenticationFailed("Failed to map the username to a DN.") try: sticky = self.settings.BIND_AS_AUTHENTICATING_USER self._bind_as(self.dn, password, sticky=sticky) except ldap.INVALID_CREDENTIALS: raise self.AuthenticationFailed("User DN/password rejected by LDAP server.") def _load_user_attrs(self): if self.dn is not None: search = LDAPSearch(self.dn, ldap.SCOPE_BASE) results = search.execute(self.connection) if results is not None and len(results) > 0: self._user_attrs = results[0][1] def _load_user_dn(self): """ Populates self._user_dn with the distinguished name of our user. This will either construct the DN from a template in AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it. """ if self._using_simple_bind_mode(): self._construct_simple_user_dn() else: self._search_for_user_dn() def _using_simple_bind_mode(self): return (self.settings.USER_DN_TEMPLATE is not None) def _construct_simple_user_dn(self): template = self.settings.USER_DN_TEMPLATE username = ldap.dn.escape_dn_chars(self._username) self._user_dn = template % {'user': username} def _search_for_user_dn(self): """ Searches the directory for a user matching AUTH_LDAP_USER_SEARCH. Populates self._user_dn and self._user_attrs. """ search = self.settings.USER_SEARCH if search is None: raise ImproperlyConfigured('AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.') results = search.execute(self.connection, {'user': self._username}) if results is not None and len(results) == 1: (self._user_dn, self._user_attrs) = results[0] def _check_requirements(self): """ Checks all authentication requirements beyond credentials. Raises AuthenticationFailed on failure. """ self._check_required_group() self._check_denied_group() def _check_required_group(self): """ Returns True if the group requirement (AUTH_LDAP_REQUIRE_GROUP) is met. Always returns True if AUTH_LDAP_REQUIRE_GROUP is None. """ required_group_dn = self.settings.REQUIRE_GROUP if required_group_dn is not None: is_member = self._get_groups().is_member_of(required_group_dn) if not is_member: raise self.AuthenticationFailed("User is not a member of AUTH_LDAP_REQUIRE_GROUP") return True def _check_denied_group(self): """ Returns True if the negative group requirement (AUTH_LDAP_DENY_GROUP) is met. Always returns True if AUTH_LDAP_DENY_GROUP is None. """ denied_group_dn = self.settings.DENY_GROUP if denied_group_dn is not None: is_member = self._get_groups().is_member_of(denied_group_dn) if is_member: raise self.AuthenticationFailed("User is a member of AUTH_LDAP_DENY_GROUP") return True # # User management # def _get_or_create_user(self, force_populate=False): """ Loads the User model object from the database or creates it if it doesn't exist. Also populates the fields, subject to AUTH_LDAP_ALWAYS_UPDATE_USER. """ save_user = False username = self.backend.ldap_to_django_username(self._username) 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 self.settings.ALWAYS_UPDATE_USER or created if created: logger.debug("Created Django user %s", username) self._user.set_unusable_password() save_user = True if should_populate: logger.debug("Populating Django user %s", username) self._populate_user() save_user = True if self.settings.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() # We populate the profile after the user model is saved to give the # 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): """ Populates our User object with information from the LDAP directory. """ self._populate_user_from_attributes() self._populate_user_from_group_memberships() def _populate_user_from_attributes(self): for field, attr in self.settings.USER_ATTR_MAP.iteritems(): try: setattr(self._user, field, self.attrs[attr][0]) except StandardError: logger.warning("%s does not have a value for the attribute %s", self.dn, attr) def _populate_user_from_group_memberships(self): for field, group_dns in self.settings.USER_FLAGS_BY_GROUP.iteritems(): if isinstance(group_dns, basestring): group_dns = [group_dns] value = any(self._get_groups().is_member_of(dn) for dn in group_dns) setattr(self._user, field, value) def _populate_and_save_user_profile(self): """ Populates a User profile object with fields from the LDAP directory. """ try: profile = self._user.get_profile() save_profile = False 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 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): logger.debug("Django user %s does not have a profile to populate", get_user_username(self._user)) def _populate_profile_from_attributes(self, profile): """ Populate the given profile object from AUTH_LDAP_PROFILE_ATTR_MAP. Returns True if the profile was modified. """ save_profile = False for field, attr in self.settings.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 StandardError: logger.warning("%s does not have a value for the attribute %s", self.dn, attr) return save_profile def _populate_profile_from_group_memberships(self, profile): """ Populate the given profile object from AUTH_LDAP_PROFILE_FLAGS_BY_GROUP. Returns True if the profile was modified. """ save_profile = False for field, group_dns in self.settings.PROFILE_FLAGS_BY_GROUP.iteritems(): if isinstance(group_dns, basestring): group_dns = [group_dns] value = any(self._get_groups().is_member_of(dn) for dn in group_dns) setattr(profile, field, value) save_profile = True return save_profile def _mirror_groups(self): """ Mirrors the user's LDAP groups in the Django database and updates the user's membership. """ group_names = self._get_groups().get_group_names() groups = [Group.objects.get_or_create(name=group_name)[0] for group_name in group_names] self._user.groups = groups # # Group information # def _load_group_permissions(self): """ Populates self._group_permissions based on LDAP group membership and Django group permissions. """ group_names = self._get_groups().get_group_names() perms = Permission.objects.filter(group__name__in=group_names) perms = perms.values_list('content_type__app_label', 'codename') perms = perms.order_by() self._group_permissions = set(["%s.%s" % (ct, name) for ct, name in perms]) def _get_groups(self): """ Returns an _LDAPUserGroups object, which can determine group membership. """ if self._groups is None: self._groups = _LDAPUserGroups(self) return self._groups # # LDAP connection # def _bind(self): """ Binds to the LDAP server with AUTH_LDAP_BIND_DN and AUTH_LDAP_BIND_PASSWORD. """ self._bind_as(self.settings.BIND_DN, self.settings.BIND_PASSWORD, sticky=True) def _bind_as(self, bind_dn, bind_password, sticky=False): """ Binds to the LDAP server with the given credentials. This does not trap exceptions. If sticky is True, then we will consider the connection to be bound for the life of this object. If False, then the caller only wishes to test the credentials, after which the connection will be considered unbound. """ self._get_connection().simple_bind_s(bind_dn.encode('utf-8'), bind_password.encode('utf-8')) self._connection_bound = sticky def _get_connection(self): """ Returns our cached LDAPObject, which may or may not be bound. """ if self._connection is None: uri = self.settings.SERVER_URI if callable(uri): uri = uri() self._connection = self.backend.ldap.initialize(uri) for opt, value in self.settings.CONNECTION_OPTIONS.iteritems(): self._connection.set_option(opt, value) if self.settings.START_TLS: logger.debug("Initiating TLS") self._connection.start_tls_s() return self._connection class _LDAPUserGroups(object): """ Represents the set of groups that a user belongs to. """ def __init__(self, ldap_user): self.settings = ldap_user.settings self._ldap_user = ldap_user self._group_type = None self._group_search = None self._group_infos = None self._group_dns = None self._group_names = None self._init_group_settings() def _init_group_settings(self): """ Loads the settings we need to deal with groups. Raises ImproperlyConfigured if anything's not right. """ self._group_type = self.settings.GROUP_TYPE if self._group_type is None: raise ImproperlyConfigured("AUTH_LDAP_GROUP_TYPE must be an LDAPGroupType instance.") self._group_search = self.settings.GROUP_SEARCH if self._group_search is None: raise ImproperlyConfigured("AUTH_LDAP_GROUP_SEARCH must be an LDAPSearch instance.") def get_group_names(self): """ Returns the set of Django group names that this user belongs to by virtue of LDAP group memberships. """ if self._group_names is None: self._load_cached_attr("_group_names") if self._group_names is None: group_infos = self._get_group_infos() self._group_names = set( self._group_type.group_name_from_info(group_info) for group_info in group_infos ) self._cache_attr("_group_names") return self._group_names def is_member_of(self, group_dn): """ Returns true if our user is a member of the given group. """ is_member = None # Normalize the DN group_dn = group_dn.lower() # If we have self._group_dns, we'll use it. Otherwise, we'll try to # avoid the cost of loading it. if self._group_dns is None: is_member = self._group_type.is_member(self._ldap_user, group_dn) if is_member is None: is_member = (group_dn in self.get_group_dns()) logger.debug("%s is%sa member of %s", self._ldap_user.dn, is_member and " " or " not ", group_dn) return is_member def get_group_dns(self): """ Returns a (cached) set of the distinguished names in self._group_infos. """ if self._group_dns is None: group_infos = self._get_group_infos() self._group_dns = set(group_info[0] for group_info in group_infos) return self._group_dns def _get_group_infos(self): """ Returns a (cached) list of group_info structures for the groups that our user is a member of. """ if self._group_infos is None: self._group_infos = self._group_type.user_groups(self._ldap_user, self._group_search) return self._group_infos def _load_cached_attr(self, attr_name): if self.settings.CACHE_GROUPS: key = self._cache_key(attr_name) value = cache.get(key) setattr(self, attr_name, value) def _cache_attr(self, attr_name): if self.settings.CACHE_GROUPS: key = self._cache_key(attr_name) value = getattr(self, attr_name, None) cache.set(key, value, self.settings.GROUP_CACHE_TIMEOUT) def _cache_key(self, attr_name): """ Memcache keys can't have spaces in them, so we'll remove them from the DN for maximum compatibility. """ dn = self._ldap_user.dn.replace(' ', '%20') key = u'auth_ldap.%s.%s.%s' % (self.__class__.__name__, attr_name, dn) return key class LDAPSettings(object): """ This is a simple class to take the place of the global settings object. An instance will contain all of our settings as attributes, with default values if they are not specified by the configuration. """ defaults = { 'ALWAYS_UPDATE_USER': True, 'AUTHORIZE_ALL_USERS': False, 'BIND_AS_AUTHENTICATING_USER': False, 'BIND_DN': '', 'BIND_PASSWORD': '', 'CACHE_GROUPS': False, 'CONNECTION_OPTIONS': {}, 'DENY_GROUP': None, 'FIND_GROUP_PERMS': False, 'GROUP_CACHE_TIMEOUT': None, 'GROUP_SEARCH': None, 'GROUP_TYPE': None, 'MIRROR_GROUPS': False, 'PERMIT_EMPTY_PASSWORD': False, 'PROFILE_ATTR_MAP': {}, 'PROFILE_FLAGS_BY_GROUP': {}, 'REQUIRE_GROUP': None, 'SERVER_URI': 'ldap://localhost', 'START_TLS': False, 'USER_ATTR_MAP': {}, 'USER_DN_TEMPLATE': None, 'USER_FLAGS_BY_GROUP': {}, 'USER_SEARCH': None, } def __init__(self, prefix='AUTH_LDAP_'): """ Loads our settings from django.conf.settings, applying defaults for any that are omitted. """ from django.conf import settings for name, default in self.defaults.iteritems(): value = getattr(settings, prefix + name, default) setattr(self, name, value) django-auth-ldap-1.1.8/django_auth_ldap/config.py 0000644 0000766 0000024 00000047265 12273243330 022547 0 ustar psagers staff 0000000 0000000 # Copyright (c) 2009, Peter Sagerson # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # - Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ This module contains classes that will be needed for configuration of LDAP authentication. Unlike backend.py, this is safe to import into settings.py. Please see the docstring on the backend module for more information, including notes on naming conventions. """ import ldap import logging import pprint class _LDAPConfig(object): """ A private class that loads and caches some global objects. """ ldap = None logger = None _ldap_configured = False def get_ldap(cls, global_options=None): """ Returns the ldap module. The unit test harness will assign a mock object to _LDAPConfig.ldap. It is imperative that the ldap module not be imported anywhere else so that the unit tests will pass in the absence of python-ldap. """ if cls.ldap is None: import ldap.filter # Support for python-ldap < 2.0.6 try: import ldap.dn except ImportError: from django_auth_ldap import dn ldap.dn = dn cls.ldap = ldap # Apply global LDAP options once if (not cls._ldap_configured) and (global_options is not None): for opt, value in global_options.iteritems(): cls.ldap.set_option(opt, value) cls._ldap_configured = True return cls.ldap get_ldap = classmethod(get_ldap) def get_logger(cls): """ Initializes and returns our logger instance. """ if cls.logger is None: class NullHandler(logging.Handler): def emit(self, record): pass cls.logger = logging.getLogger('django_auth_ldap') cls.logger.addHandler(NullHandler()) return cls.logger get_logger = classmethod(get_logger) # Our global logger logger = _LDAPConfig.get_logger() class LDAPSearch(object): """ Public class that holds a set of LDAP search parameters. Objects of this class should be considered immutable. Only the initialization method is documented for configuration purposes. Internal clients may use the other methods to refine and execute the search. """ def __init__(self, base_dn, scope, filterstr=u'(objectClass=*)'): """ These parameters are the same as the first three parameters to ldap.search_s. """ self.base_dn = base_dn self.scope = scope self.filterstr = filterstr self.ldap = _LDAPConfig.get_ldap() def search_with_additional_terms(self, term_dict, escape=True): """ Returns a new search object with additional search terms and-ed to the filter string. term_dict maps attribute names to assertion values. If you don't want the values escaped, pass escape=False. """ term_strings = [self.filterstr] for name, value in term_dict.iteritems(): if escape: value = self.ldap.filter.escape_filter_chars(value) term_strings.append(u'(%s=%s)' % (name, value)) filterstr = u'(&%s)' % ''.join(term_strings) return self.__class__(self.base_dn, self.scope, filterstr) def search_with_additional_term_string(self, filterstr): """ Returns a new search object with filterstr and-ed to the original filter string. The caller is responsible for passing in a properly escaped string. """ filterstr = u'(&%s%s)' % (self.filterstr, filterstr) return self.__class__(self.base_dn, self.scope, filterstr) def execute(self, connection, filterargs=()): """ Executes the search on the given connection (an LDAPObject). filterargs is an object that will be used for expansion of the filter string. The python-ldap library returns utf8-encoded strings. For the sake of sanity, this method will decode all result strings and return them as Unicode. """ try: filterstr = self.filterstr % filterargs results = connection.search_s(self.base_dn.encode('utf-8'), self.scope, filterstr.encode('utf-8')) except ldap.LDAPError, e: results = [] logger.error(u"search_s('%s', %d, '%s') raised %s" % (self.base_dn, self.scope, filterstr, pprint.pformat(e))) return self._process_results(results) def _begin(self, connection, filterargs=()): """ Begins an asynchronous search and returns the message id to retrieve the results. """ try: filterstr = self.filterstr % filterargs msgid = connection.search(self.base_dn.encode('utf-8'), self.scope, filterstr.encode('utf-8')) except ldap.LDAPError, e: msgid = None logger.error(u"search('%s', %d, '%s') raised %s" % (self.base_dn, self.scope, filterstr, pprint.pformat(e))) return msgid def _results(self, connection, msgid): """ Returns the result of a previous asynchronous query. """ try: kind, results = connection.result(msgid) if kind != ldap.RES_SEARCH_RESULT: results = [] except ldap.LDAPError, e: results = [] logger.error(u"result(%d) raised %s" % (msgid, pprint.pformat(e))) return self._process_results(results) def _process_results(self, results): """ Returns a sanitized copy of raw LDAP results. This scrubs out references, decodes utf8, normalizes DNs, etc. """ results = filter(lambda r: r[0] is not None, results) results = _DeepStringCoder('utf-8').decode(results) # The normal form of a DN is lower case. results = map(lambda r: (r[0].lower(), r[1]), results) result_dns = [result[0] for result in results] logger.debug(u"search_s('%s', %d, '%s') returned %d objects: %s" % (self.base_dn, self.scope, self.filterstr, len(result_dns), "; ".join(result_dns))) return results class LDAPSearchUnion(object): """ A compound search object that returns the union of the results. Instantiate it with one or more LDAPSearch objects. """ def __init__(self, *args): self.searches = args self.ldap = _LDAPConfig.get_ldap() def search_with_additional_terms(self, term_dict, escape=True): searches = [s.search_with_additional_terms(term_dict, escape) for s in self.searches] return self.__class__(*searches) def search_with_additional_term_string(self, filterstr): searches = [s.search_with_additional_term_string(filterstr) for s in self.searches] return self.__class__(*searches) def execute(self, connection, filterargs=()): msgids = [search._begin(connection, filterargs) for search in self.searches] results = {} for search, msgid in zip(self.searches, msgids): result = search._results(connection, msgid) results.update(dict(result)) return results.items() class _DeepStringCoder(object): """ Encodes and decodes strings in a nested structure of lists, tuples, and dicts. This is helpful when interacting with the Unicode-unaware python-ldap. """ def __init__(self, encoding): self.encoding = encoding self.ldap = _LDAPConfig.get_ldap() def decode(self, value): try: if isinstance(value, str): value = value.decode(self.encoding) elif isinstance(value, list): value = self._decode_list(value) elif isinstance(value, tuple): value = tuple(self._decode_list(value)) elif isinstance(value, dict): value = self._decode_dict(value) except UnicodeDecodeError: pass return value def _decode_list(self, value): return [self.decode(v) for v in value] def _decode_dict(self, value): # Attribute dictionaries should be case-insensitive. python-ldap # defines this, although for some reason, it doesn't appear to use it # for search results. decoded = self.ldap.cidict.cidict() for k, v in value.iteritems(): decoded[self.decode(k)] = self.decode(v) return decoded class LDAPGroupType(object): """ This is an abstract base class for classes that determine LDAP group membership. A group can mean many different things in LDAP, so we will need a concrete subclass for each grouping mechanism. Clients may subclass this if they have a group mechanism that is not handled by a built-in implementation. name_attr is the name of the LDAP attribute from which we will take the Django group name. Subclasses in this file must use self.ldap to access the python-ldap module. This will be a mock object during unit tests. """ def __init__(self, name_attr="cn"): self.name_attr = name_attr self.ldap = _LDAPConfig.get_ldap() def user_groups(self, ldap_user, group_search): """ Returns a list of group_info structures, each one a group to which ldap_user belongs. group_search is an LDAPSearch object that returns all of the groups that the user might belong to. Typical implementations will apply additional filters to group_search and return the results of the search. ldap_user represents the user and has the following three properties: dn: the distinguished name attrs: a dictionary of LDAP attributes (with lists of values) connection: an LDAPObject that has been bound with credentials This is the primitive method in the API and must be implemented. """ return [] def is_member(self, ldap_user, group_dn): """ This method is an optimization for determining group membership without loading all of the user's groups. Subclasses that are able to do this may return True or False. ldap_user is as above. group_dn is the distinguished name of the group in question. The base implementation returns None, which means we don't have enough information. The caller will have to call user_groups() instead and look for group_dn in the results. """ return None def group_name_from_info(self, group_info): """ Given the (DN, attrs) 2-tuple of an LDAP group, this returns the name of the Django group. This may return None to indicate that a particular LDAP group has no corresponding Django group. The base implementation returns the value of the cn attribute, or whichever attribute was given to __init__ in the name_attr parameter. """ try: name = group_info[1][self.name_attr][0] except (KeyError, IndexError): name = None return name class PosixGroupType(LDAPGroupType): """ An LDAPGroupType subclass that handles groups of class posixGroup. """ def user_groups(self, ldap_user, group_search): """ Searches for any group that is either the user's primary or contains the user as a member. """ groups = [] try: user_uid = ldap_user.attrs['uid'][0] if 'gidNumber' in ldap_user.attrs: user_gid = ldap_user.attrs['gidNumber'][0] filterstr = u'(|(gidNumber=%s)(memberUid=%s))' % ( self.ldap.filter.escape_filter_chars(user_gid), self.ldap.filter.escape_filter_chars(user_uid) ) else: filterstr = u'(memberUid=%s)' % ( self.ldap.filter.escape_filter_chars(user_uid), ) search = group_search.search_with_additional_term_string(filterstr) groups = search.execute(ldap_user.connection) except (KeyError, IndexError): pass return groups def is_member(self, ldap_user, group_dn): """ Returns True if the group is the user's primary group or if the user is listed in the group's memberUid attribute. """ try: user_uid = ldap_user.attrs['uid'][0] try: is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'memberUid', user_uid.encode('utf-8')) except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): is_member = False if not is_member: try: user_gid = ldap_user.attrs['gidNumber'][0] is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'gidNumber', user_gid.encode('utf-8')) except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): is_member = False except (KeyError, IndexError): is_member = False return is_member class MemberDNGroupType(LDAPGroupType): """ A group type that stores lists of members as distinguished names. """ def __init__(self, member_attr, name_attr='cn'): """ member_attr is the attribute on the group object that holds the list of member DNs. """ self.member_attr = member_attr super(MemberDNGroupType, self).__init__(name_attr) def user_groups(self, ldap_user, group_search): search = group_search.search_with_additional_terms({self.member_attr: ldap_user.dn}) groups = search.execute(ldap_user.connection) return groups def is_member(self, ldap_user, group_dn): try: result = ldap_user.connection.compare_s( group_dn.encode('utf-8'), self.member_attr.encode('utf-8'), ldap_user.dn.encode('utf-8') ) except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): result = 0 return result class NestedMemberDNGroupType(LDAPGroupType): """ A group type that stores lists of members as distinguished names and supports nested groups. There is no shortcut for is_member in this case, so it's left unimplemented. """ def __init__(self, member_attr, name_attr='cn'): """ member_attr is the attribute on the group object that holds the list of member DNs. """ self.member_attr = member_attr super(NestedMemberDNGroupType, self).__init__(name_attr) def user_groups(self, ldap_user, group_search): """ This searches for all of a user's groups from the bottom up. In other words, it returns the groups that the user belongs to, the groups that those groups belong to, etc. Circular references will be detected and pruned. """ group_info_map = {} # Maps group_dn to group_info of groups we've found member_dn_set = set([ldap_user.dn]) # Member DNs to search with next handled_dn_set = set() # Member DNs that we've already searched with while len(member_dn_set) > 0: group_infos = self.find_groups_with_any_member(member_dn_set, group_search, ldap_user.connection) new_group_info_map = dict([(info[0], info) for info in group_infos]) group_info_map.update(new_group_info_map) handled_dn_set.update(member_dn_set) # Get ready for the next iteration. To avoid cycles, we make sure # never to search with the same member DN twice. member_dn_set = set(new_group_info_map.keys()) - handled_dn_set return group_info_map.values() def find_groups_with_any_member(self, member_dn_set, group_search, connection): terms = [ u"(%s=%s)" % (self.member_attr, self.ldap.filter.escape_filter_chars(dn)) for dn in member_dn_set ] filterstr = u"(|%s)" % "".join(terms) search = group_search.search_with_additional_term_string(filterstr) return search.execute(connection) class GroupOfNamesType(MemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class groupOfNames. """ def __init__(self, name_attr='cn'): super(GroupOfNamesType, self).__init__('member', name_attr) class NestedGroupOfNamesType(NestedMemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class groupOfNames with nested group references. """ def __init__(self, name_attr='cn'): super(NestedGroupOfNamesType, self).__init__('member', name_attr) class GroupOfUniqueNamesType(MemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class groupOfUniqueNames. """ def __init__(self, name_attr='cn'): super(GroupOfUniqueNamesType, self).__init__('uniqueMember', name_attr) class NestedGroupOfUniqueNamesType(NestedMemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class groupOfUniqueNames with nested group references. """ def __init__(self, name_attr='cn'): super(NestedGroupOfUniqueNamesType, self).__init__('uniqueMember', name_attr) class ActiveDirectoryGroupType(MemberDNGroupType): """ An LDAPGroupType subclass that handles Active Directory groups. """ def __init__(self, name_attr='cn'): super(ActiveDirectoryGroupType, self).__init__('member', name_attr) class NestedActiveDirectoryGroupType(NestedMemberDNGroupType): """ An LDAPGroupType subclass that handles Active Directory groups with nested group references. """ def __init__(self, name_attr='cn'): super(NestedActiveDirectoryGroupType, self).__init__('member', name_attr) class OrganizationalRoleGroupType(MemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class organizationalRole. """ def __init__(self, name_attr='cn'): super(OrganizationalRoleGroupType, self).__init__('roleOccupant', name_attr) class NestedOrganizationalRoleGroupType(NestedMemberDNGroupType): """ An LDAPGroupType subclass that handles groups of class OrganizationalRoleGroupType with nested group references. """ def __init__(self, name_attr='cn'): super(NestedOrganizationalRoleGroupType, self).__init__('roleOccupant', name_attr) django-auth-ldap-1.1.8/django_auth_ldap/dn.py 0000644 0000766 0000024 00000003067 12242245151 021672 0 ustar psagers staff 0000000 0000000 # Copyright (c) 2009, Peter Sagerson # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # - Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ This is an ldap.dn replacement for old versions of python-ldap. It contains (often naive) implementations of the methods we care about. """ def escape_dn_chars(dn): "Old versions of python-ldap won't get DN escaping. Use with care." return dn django-auth-ldap-1.1.8/django_auth_ldap/models.py 0000644 0000766 0000024 00000001531 12242245151 022546 0 ustar psagers staff 0000000 0000000 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 authentication backend itself. """ user = models.OneToOneField('auth.User') is_special = models.BooleanField(default=False) populated = models.BooleanField(default=False) django-auth-ldap-1.1.8/django_auth_ldap/tests.py 0000644 0000766 0000024 00000124323 12273243253 022437 0 ustar psagers staff 0000000 0000000 # coding: utf-8 # Copyright (c) 2009, Peter Sagerson # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # - Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # - Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from copy import deepcopy import logging import pickle import ldap try: import mockldap except ImportError: mockldap = None from django.conf import settings import django.db.models.signals from django.contrib.auth.models import User, Permission, Group from django.test import TestCase from django.utils import unittest 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 LDAPSearch, LDAPSearchUnion from django_auth_ldap.config import PosixGroupType, MemberDNGroupType, NestedMemberDNGroupType from django_auth_ldap.config import GroupOfNamesType class TestSettings(backend.LDAPSettings): """ A replacement for backend.LDAPSettings that does not load settings from django.conf. """ def __init__(self, **kwargs): for name, default in self.defaults.iteritems(): value = kwargs.get(name, default) setattr(self, name, value) class LDAPTest(TestCase): top = ("o=test", {"o": "test"}) people = ("ou=people,o=test", {"ou": "people"}) groups = ("ou=groups,o=test", {"ou": "groups"}) moregroups = ("ou=moregroups,o=test", {"ou": "moregroups"}) alice = ("uid=alice,ou=people,o=test", { "uid": ["alice"], "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"], "userPassword": ["password"], "uidNumber": ["1000"], "gidNumber": ["1000"], "givenName": ["Alice"], "sn": ["Adams"] }) bob = ("uid=bob,ou=people,o=test", { "uid": ["bob"], "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"], "userPassword": ["password"], "uidNumber": ["1001"], "gidNumber": ["50"], "givenName": ["Robert"], "sn": ["Barker"] }) dressler = (u"uid=dreßler,ou=people,o=test".encode('utf-8'), { "uid": [u"dreßler".encode('utf-8')], "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"], "userPassword": ["password"], "uidNumber": ["1002"], "gidNumber": ["50"], "givenName": ["Wolfgang"], "sn": [u"Dreßler".encode('utf-8')] }) nobody = ("uid=nobody,ou=people,o=test", { "uid": ["nobody"], "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"], "userPassword": ["password"], "binaryAttr": ["\xb2"] # Invalid UTF-8 }) # posixGroup objects active_px = ("cn=active_px,ou=groups,o=test", { "cn": ["active_px"], "objectClass": ["posixGroup"], "gidNumber": ["1000"], "memberUid": [], }) staff_px = ("cn=staff_px,ou=groups,o=test", { "cn": ["staff_px"], "objectClass": ["posixGroup"], "gidNumber": ["1001"], "memberUid": ["alice"], }) superuser_px = ("cn=superuser_px,ou=groups,o=test", { "cn": ["superuser_px"], "objectClass": ["posixGroup"], "gidNumber": ["1002"], "memberUid": ["alice"], }) # groupOfNames groups empty_gon = ("cn=empty_gon,ou=groups,o=test", { "cn": ["empty_gon"], "objectClass": ["groupOfNames"], "member": [] }) active_gon = ("cn=active_gon,ou=groups,o=test", { "cn": ["active_gon"], "objectClass": ["groupOfNames"], "member": ["uid=alice,ou=people,o=test"] }) staff_gon = ("cn=staff_gon,ou=groups,o=test", { "cn": ["staff_gon"], "objectClass": ["groupOfNames"], "member": ["uid=alice,ou=people,o=test"] }) superuser_gon = ("cn=superuser_gon,ou=groups,o=test", { "cn": ["superuser_gon"], "objectClass": ["groupOfNames"], "member": ["uid=alice,ou=people,o=test"] }) other_gon = ("cn=other_gon,ou=moregroups,o=test", { "cn": ["other_gon"], "objectClass": ["groupOfNames"], "member": ["uid=bob,ou=people,o=test"] }) # Nested groups with a circular reference parent_gon = ("cn=parent_gon,ou=groups,o=test", { "cn": ["parent_gon"], "objectClass": ["groupOfNames"], "member": ["cn=nested_gon,ou=groups,o=test"] }) nested_gon = ("CN=nested_gon,ou=groups,o=test", { "cn": ["nested_gon"], "objectClass": ["groupOfNames"], "member": [ "uid=alice,ou=people,o=test", "cn=circular_gon,ou=groups,o=test" ] }) circular_gon = ("cn=circular_gon,ou=groups,o=test", { "cn": ["circular_gon"], "objectClass": ["groupOfNames"], "member": ["cn=parent_gon,ou=groups,o=test"] }) directory = dict([top, people, groups, moregroups, alice, bob, dressler, nobody, active_px, staff_px, superuser_px, empty_gon, active_gon, staff_gon, superuser_gon, other_gon, parent_gon, nested_gon, circular_gon]) @classmethod def configure_logger(cls): logger = logging.getLogger('django_auth_ldap') formatter = logging.Formatter("LDAP auth - %(levelname)s - %(message)s") handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.CRITICAL) @classmethod def setUpClass(cls): cls.configure_logger() cls.mockldap = mockldap.MockLdap(cls.directory) @classmethod def tearDownClass(cls): del cls.mockldap def setUp(self): self.mockldap.start() self.ldapobj = self.mockldap['ldap://localhost'] self.backend = backend.LDAPBackend() self.backend.ldap # Force global configuration def tearDown(self): self.mockldap.stop() del self.ldapobj def test_options(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', CONNECTION_OPTIONS={'opt1': 'value1'} ) self.backend.authenticate(username='alice', password='password') self.assertEqual(self.ldapobj.get_option('opt1'), 'value1') def test_callable_server_uri(self): self._init_settings( SERVER_URI=lambda: 'ldap://ldap.example.com', USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) self.backend.authenticate(username='alice', password='password') ldapobj = self.mockldap['ldap://ldap.example.com'] self.assertEqual( ldapobj.methods_called(with_args=True), [('initialize', ('ldap://ldap.example.com',), {}), ('simple_bind_s', ('uid=alice,ou=people,o=test', 'password'), {})] ) def test_simple_bind(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) user_count = User.objects.count() user = self.backend.authenticate(username='alice', password='password') self.assertTrue(not user.has_usable_password()) self.assertEqual(user.username, 'alice') self.assertEqual(User.objects.count(), user_count + 1) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s'] ) def test_new_user_lowercase(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) user_count = User.objects.count() user = self.backend.authenticate(username='Alice', password='password') self.assertTrue(not user.has_usable_password()) self.assertEqual(user.username, 'alice') self.assertEqual(User.objects.count(), user_count + 1) self.assertEqual( self.ldapobj.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.assertTrue(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.assertTrue(isinstance(user, TestUser)) def test_new_user_whitespace(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) user_count = User.objects.count() user = self.backend.authenticate(username=' alice', password='password') user = self.backend.authenticate(username='alice ', password='password') self.assertTrue(not user.has_usable_password()) self.assertEqual(user.username, 'alice') self.assertEqual(User.objects.count(), user_count + 1) def test_simple_bind_bad_user(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) user_count = User.objects.count() user = self.backend.authenticate(username='evil_alice', password='password') self.assertTrue(user is None) self.assertEqual(User.objects.count(), user_count) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s'] ) def test_simple_bind_bad_password(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) user_count = User.objects.count() user = self.backend.authenticate(username='alice', password='bogus') self.assertTrue(user is None) self.assertEqual(User.objects.count(), user_count) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s'] ) def test_existing_user(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) User.objects.create(username='alice') user_count = User.objects.count() user = self.backend.authenticate(username='alice', password='password') # Make sure we only created one user self.assertTrue(user is not None) self.assertEqual(User.objects.count(), user_count) def test_existing_user_insensitive(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)' ) ) # mockldap doesn't handle case-insensitive matching properly. self.ldapobj.search_s.seed('ou=people,o=test', ldap.SCOPE_SUBTREE, '(uid=Alice)')([self.alice]) User.objects.create(username='alice') user = self.backend.authenticate(username='Alice', password='password') self.assertTrue(user is not None) self.assertEqual(user.username, 'alice') self.assertEqual(User.objects.count(), 1) def test_convert_username(self): class MyBackend(backend.LDAPBackend): def ldap_to_django_username(self, username): return 'ldap_%s' % username def django_to_ldap_username(self, username): return username[5:] self.backend = MyBackend() self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) user_count = User.objects.count() user1 = self.backend.authenticate(username='alice', password='password') user2 = self.backend.get_user(user1.pk) self.assertEqual(User.objects.count(), user_count + 1) self.assertEqual(user1.username, 'ldap_alice') self.assertEqual(user1.ldap_user._username, 'alice') self.assertEqual(user1.ldap_username, 'alice') self.assertEqual(user2.username, 'ldap_alice') self.assertEqual(user2.ldap_user._username, 'alice') self.assertEqual(user2.ldap_username, 'alice') def test_search_bind(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)' ) ) user_count = User.objects.count() user = self.backend.authenticate(username='alice', password='password') self.assertTrue(user is not None) self.assertEqual(User.objects.count(), user_count + 1) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'] ) def test_search_bind_no_user(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, '(cn=%(user)s)' ) ) user = self.backend.authenticate(username='alice', password='password') self.assertTrue(user is None) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'search_s'] ) def test_search_bind_multiple_users(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=*)' ) ) user = self.backend.authenticate(username='alice', password='password') self.assertTrue(user is None) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'search_s'] ) def test_search_bind_bad_password(self): self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)' ) ) user = self.backend.authenticate(username='alice', password='bogus') self.assertTrue(user is None) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'] ) def test_search_bind_with_credentials(self): self._init_settings( BIND_DN='uid=bob,ou=people,o=test', BIND_PASSWORD='password', USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)' ) ) user = self.backend.authenticate(username='alice', password='password') self.assertTrue(user is not None) self.assertTrue(user.ldap_user is not None) self.assertEqual(user.ldap_user.dn, self.alice[0]) self.assertEqual(user.ldap_user.attrs, ldap.cidict.cidict(self.alice[1])) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'] ) def test_search_bind_with_bad_credentials(self): self._init_settings( BIND_DN='uid=bob,ou=people,o=test', BIND_PASSWORD='bogus', USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)' ) ) user = self.backend.authenticate(username='alice', password='password') self.assertTrue(user is None) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s'] ) def test_unicode_user(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'} ) user = self.backend.authenticate(username=u'dreßler', password='password') self.assertTrue(user is not None) self.assertEqual(user.username, u'dreßler') self.assertEqual(user.last_name, u'Dreßler') def test_cidict(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', ) user = self.backend.authenticate(username="alice", password="password") self.assertTrue(isinstance(user.ldap_user.attrs, ldap.cidict.cidict)) def test_populate_user(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'} ) user = self.backend.authenticate(username='alice', password='password') self.assertEqual(user.username, 'alice') self.assertEqual(user.first_name, 'Alice') self.assertEqual(user.last_name, 'Adams') # init, bind as user, bind anonymous, lookup user attrs self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'simple_bind_s', 'search_s'] ) def test_bind_as_user(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'}, BIND_AS_AUTHENTICATING_USER=True, ) user = self.backend.authenticate(username='alice', password='password') self.assertEqual(user.username, 'alice') self.assertEqual(user.first_name, 'Alice') self.assertEqual(user.last_name, 'Adams') # init, bind as user, lookup user attrs self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'search_s'] ) def test_signal_populate_user(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) def handle_populate_user(sender, **kwargs): self.assertTrue('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.assertTrue(user.populate_user_handled) def test_signal_populate_user_profile(self): settings.AUTH_PROFILE_MODULE = 'django_auth_ldap.TestProfile' self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' ) def handle_user_saved(sender, **kwargs): if kwargs['created']: TestProfile.objects.create(user=kwargs['instance']) def handle_populate_user_profile(sender, **kwargs): self.assertTrue('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.assertTrue(user.get_profile().populated) def test_no_update_existing(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'}, ALWAYS_UPDATE_USER=False ) User.objects.create(username='alice', first_name='Alicia', last_name='Astro') alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertEqual(alice.first_name, 'Alicia') self.assertEqual(alice.last_name, 'Astro') self.assertEqual(bob.first_name, 'Robert') self.assertEqual(bob.last_name, 'Barker') def test_require_group(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'), GROUP_TYPE=MemberDNGroupType(member_attr='member'), REQUIRE_GROUP="cn=active_gon,ou=groups,o=test" ) alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertTrue(alice is not None) self.assertTrue(bob is None) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'simple_bind_s', 'compare_s', 'initialize', 'simple_bind_s', 'simple_bind_s', 'compare_s'] ) def test_group_union(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearchUnion( LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'), LDAPSearch('ou=moregroups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)') ), GROUP_TYPE=MemberDNGroupType(member_attr='member'), REQUIRE_GROUP="cn=other_gon,ou=moregroups,o=test" ) alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertTrue(alice is None) self.assertTrue(bob is not None) self.assertEqual(bob.ldap_user.group_names, set(['other_gon'])) def test_nested_group_union(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearchUnion( LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'), LDAPSearch('ou=moregroups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)') ), GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'), REQUIRE_GROUP="cn=other_gon,ou=moregroups,o=test" ) alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertTrue(alice is None) self.assertTrue(bob is not None) self.assertEqual(bob.ldap_user.group_names, set(['other_gon'])) def test_denied_group(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), DENY_GROUP="cn=active_gon,ou=groups,o=test" ) alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertTrue(alice is None) self.assertTrue(bob is not None) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'simple_bind_s', 'compare_s', 'initialize', 'simple_bind_s', 'simple_bind_s', 'compare_s'] ) def test_group_dns(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), ) alice = self.backend.authenticate(username='alice', password='password') self.assertEqual(alice.ldap_user.group_dns, set((g[0].lower() for g in [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon]))) def test_group_names(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), ) alice = self.backend.authenticate(username='alice', password='password') self.assertEqual(alice.ldap_user.group_names, set(['active_gon', 'staff_gon', 'superuser_gon', 'nested_gon'])) def test_dn_group_membership(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), USER_FLAGS_BY_GROUP={ 'is_active': "cn=active_gon,ou=groups,o=test", 'is_staff': ["cn=empty_gon,ou=groups,o=test", "cn=staff_gon,ou=groups,o=test"], 'is_superuser': "cn=superuser_gon,ou=groups,o=test" } ) alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertTrue(alice.is_active) self.assertTrue(alice.is_staff) self.assertTrue(alice.is_superuser) self.assertTrue(not bob.is_active) self.assertTrue(not bob.is_staff) self.assertTrue(not bob.is_superuser) def test_posix_membership(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=PosixGroupType(), USER_FLAGS_BY_GROUP={ 'is_active': "cn=active_px,ou=groups,o=test", 'is_staff': "cn=staff_px,ou=groups,o=test", 'is_superuser': "cn=superuser_px,ou=groups,o=test" } ) alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertTrue(alice.is_active) self.assertTrue(alice.is_staff) self.assertTrue(alice.is_superuser) self.assertTrue(not bob.is_active) self.assertTrue(not bob.is_staff) self.assertTrue(not bob.is_superuser) def test_nested_dn_group_membership(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'), USER_FLAGS_BY_GROUP={ 'is_active': "cn=parent_gon,ou=groups,o=test", 'is_staff': "cn=parent_gon,ou=groups,o=test", } ) alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertTrue(alice.is_active) self.assertTrue(alice.is_staff) self.assertTrue(not bob.is_active) self.assertTrue(not bob.is_staff) def test_posix_missing_attributes(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=PosixGroupType(), USER_FLAGS_BY_GROUP={ 'is_active': "cn=active_px,ou=groups,o=test" } ) nobody = self.backend.authenticate(username='nobody', password='password') self.assertTrue(not nobody.is_active) def test_profile_flags(self): settings.AUTH_PROFILE_MODULE = 'django_auth_ldap.TestProfile' self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), PROFILE_FLAGS_BY_GROUP={ 'is_special': ["cn=superuser_gon,ou=groups,o=test"] } ) def handle_user_saved(sender, **kwargs): if kwargs['created']: TestProfile.objects.create(user=kwargs['instance']) django.db.models.signals.post_save.connect(handle_user_saved, sender=User) alice = self.backend.authenticate(username='alice', password='password') bob = self.backend.authenticate(username='bob', password='password') self.assertTrue(alice.get_profile().is_special) self.assertTrue(not bob.get_profile().is_special) def test_dn_group_permissions(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), FIND_GROUP_PERMS=True ) self._init_groups() alice = User.objects.create(username='alice') alice = self.backend.get_user(alice.pk) self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"])) self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"])) self.assertTrue(self.backend.has_perm(alice, "auth.add_user")) self.assertTrue(self.backend.has_module_perms(alice, "auth")) def test_empty_group_permissions(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), FIND_GROUP_PERMS=True ) self._init_groups() bob = User.objects.create(username='bob') bob = self.backend.get_user(bob.pk) self.assertEqual(self.backend.get_group_permissions(bob), set()) self.assertEqual(self.backend.get_all_permissions(bob), set()) self.assertTrue(not self.backend.has_perm(bob, "auth.add_user")) self.assertTrue(not self.backend.has_module_perms(bob, "auth")) def test_posix_group_permissions(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)'), GROUP_TYPE=PosixGroupType(), FIND_GROUP_PERMS=True ) self._init_groups() alice = User.objects.create(username='alice') alice = self.backend.get_user(alice.pk) self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"])) self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"])) self.assertTrue(self.backend.has_perm(alice, "auth.add_user")) self.assertTrue(self.backend.has_module_perms(alice, "auth")) def test_posix_group_permissions_no_gid(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)'), GROUP_TYPE=PosixGroupType(), FIND_GROUP_PERMS=True ) self._init_groups() self.ldapobj.modify_s(self.alice[0], [(ldap.MOD_DELETE, 'gidNumber', None)]) self.ldapobj.modify_s(self.active_px[0], [(ldap.MOD_ADD, 'memberUid', ['alice'])]) alice = User.objects.create(username='alice') alice = self.backend.get_user(alice.pk) self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"])) self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"])) self.assertTrue(self.backend.has_perm(alice, "auth.add_user")) self.assertTrue(self.backend.has_module_perms(alice, "auth")) def test_foreign_user_permissions(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), FIND_GROUP_PERMS=True ) self._init_groups() alice = User.objects.create(username='alice') self.assertEqual(self.backend.get_group_permissions(alice), set()) def test_group_cache(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), FIND_GROUP_PERMS=True, CACHE_GROUPS=True ) self._init_groups() alice_id = User.objects.create(username='alice').pk bob_id = User.objects.create(username='bob').pk # Check permissions twice for each user for i in range(2): alice = self.backend.get_user(alice_id) self.assertEqual( self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"]) ) bob = self.backend.get_user(bob_id) self.assertEqual(self.backend.get_group_permissions(bob), set()) # Should have executed one LDAP search per user self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'search_s', 'initialize', 'simple_bind_s', 'search_s'] ) def test_group_mirroring(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)'), GROUP_TYPE=PosixGroupType(), MIRROR_GROUPS=True, ) self.assertEqual(Group.objects.count(), 0) alice = self.backend.authenticate(username='alice', password='password') self.assertEqual(Group.objects.count(), 3) self.assertEqual(set(alice.groups.all()), set(Group.objects.all())) def test_nested_group_mirroring(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE, '(objectClass=groupOfNames)'), GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'), MIRROR_GROUPS=True, ) alice = self.backend.authenticate(username='alice', password='password') self.assertEqual( set(Group.objects.all().values_list('name', flat=True)), set(['active_gon', 'staff_gon', 'superuser_gon', 'nested_gon', 'parent_gon', 'circular_gon']) ) self.assertEqual(set(alice.groups.all()), set(Group.objects.all())) def test_authorize_external_users(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), FIND_GROUP_PERMS=True, AUTHORIZE_ALL_USERS=True ) self._init_groups() alice = User.objects.create(username='alice') self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"])) def test_create_without_auth(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', ) alice = self.backend.populate_user('alice') bob = self.backend.populate_user('bob') self.assertTrue(alice is not None) self.assertEqual(alice.first_name, u"") self.assertEqual(alice.last_name, u"") self.assertTrue(alice.is_active) self.assertTrue(not alice.is_staff) self.assertTrue(not alice.is_superuser) self.assertTrue(bob is not None) self.assertEqual(bob.first_name, u"") self.assertEqual(bob.last_name, u"") self.assertTrue(bob.is_active) self.assertTrue(not bob.is_staff) self.assertTrue(not bob.is_superuser) def test_populate_without_auth(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', ALWAYS_UPDATE_USER=False, USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'}, GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=GroupOfNamesType(), USER_FLAGS_BY_GROUP={ 'is_active': "cn=active_gon,ou=groups,o=test", 'is_staff': "cn=staff_gon,ou=groups,o=test", 'is_superuser': "cn=superuser_gon,ou=groups,o=test" } ) User.objects.create(username='alice') User.objects.create(username='bob') alice = self.backend.populate_user('alice') bob = self.backend.populate_user('bob') self.assertTrue(alice is not None) self.assertEqual(alice.first_name, u"Alice") self.assertEqual(alice.last_name, u"Adams") self.assertTrue(alice.is_active) self.assertTrue(alice.is_staff) self.assertTrue(alice.is_superuser) self.assertTrue(bob is not None) self.assertEqual(bob.first_name, u"Robert") self.assertEqual(bob.last_name, u"Barker") self.assertTrue(not bob.is_active) self.assertTrue(not bob.is_staff) self.assertTrue(not bob.is_superuser) def test_populate_bogus_user(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', ) bogus = self.backend.populate_user('bogus') self.assertEqual(bogus, None) def test_start_tls_missing(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', START_TLS=False, ) self.assertTrue(not self.ldapobj.tls_enabled) self.backend.authenticate(username='alice', password='password') self.assertTrue(not self.ldapobj.tls_enabled) def test_start_tls(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', START_TLS=True, ) self.assertTrue(not self.ldapobj.tls_enabled) self.backend.authenticate(username='alice', password='password') self.assertTrue(self.ldapobj.tls_enabled) def test_null_search_results(self): """ Make sure we're not phased by referrals. """ self._init_settings( USER_SEARCH=LDAPSearch( "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)' ) ) self.backend.authenticate(username='alice', password='password') def test_union_search(self): self._init_settings( USER_SEARCH=LDAPSearchUnion( LDAPSearch("ou=groups,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)'), LDAPSearch("ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)'), ) ) alice = self.backend.authenticate(username='alice', password='password') self.assertTrue(alice is not None) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s', 'search', 'search', 'result', 'result', 'simple_bind_s'] ) def test_deny_empty_password(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', ) alice = self.backend.authenticate(username=u'alice', password=u'') self.assertEqual(alice, None) self.assertEqual(self.ldapobj.methods_called(), []) def test_permit_empty_password(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', PERMIT_EMPTY_PASSWORD=True, ) alice = self.backend.authenticate(username=u'alice', password=u'') self.assertEqual(alice, None) self.assertEqual( self.ldapobj.methods_called(), ['initialize', 'simple_bind_s'] ) def test_pickle(self): self._init_settings( USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', GROUP_SEARCH=LDAPSearch('ou=groups,o=test', ldap.SCOPE_SUBTREE), GROUP_TYPE=MemberDNGroupType(member_attr='member'), FIND_GROUP_PERMS=True ) self._init_groups() alice0 = self.backend.authenticate(username=u'alice', password=u'password') pickled = pickle.dumps(alice0, pickle.HIGHEST_PROTOCOL) alice = pickle.loads(pickled) alice.ldap_user.backend.settings = alice0.ldap_user.backend.settings self.assertTrue(alice is not None) self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"])) self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"])) self.assertTrue(self.backend.has_perm(alice, "auth.add_user")) self.assertTrue(self.backend.has_module_perms(alice, "auth")) def _init_settings(self, **kwargs): self.backend.settings = TestSettings(**kwargs) def _init_groups(self): permissions = [ Permission.objects.get(codename="add_user"), Permission.objects.get(codename="change_user") ] active_gon = Group.objects.create(name='active_gon') active_gon.permissions.add(*permissions) active_px = Group.objects.create(name='active_px') active_px.permissions.add(*permissions) # Python 2.5-compatible class decoration LDAPTest = unittest.skipIf(mockldap is None, "django_auth_ldap tests require the mockldap package.")(LDAPTest) django-auth-ldap-1.1.8/django_auth_ldap.egg-info/ 0000755 0000766 0000024 00000000000 12273247061 022411 5 ustar psagers staff 0000000 0000000 django-auth-ldap-1.1.8/django_auth_ldap.egg-info/dependency_links.txt 0000644 0000766 0000024 00000000001 12273247060 026456 0 ustar psagers staff 0000000 0000000 django-auth-ldap-1.1.8/django_auth_ldap.egg-info/PKG-INFO 0000644 0000766 0000024 00000007606 12273247060 023516 0 ustar psagers staff 0000000 0000000 Metadata-Version: 1.1 Name: django-auth-ldap Version: 1.1.8 Summary: Django LDAP authentication backend Home-page: http://bitbucket.org/psagers/django-auth-ldap/ Author: Peter Sagerson Author-email: psagers.pypi@ignorare.net License: BSD Description: This is a Django authentication backend that authenticates against an LDAP service. Configuration can be as simple as a single distinguished name template, but there are many rich configuration options for working with users, groups, and permissions. This version is tested on Python 2.6 to 2.7, Django >= 1.3, and python-ldap 2.4.13. Full documentation can be found at http://pythonhosted.org/django-auth-ldap/; following is an example configuration, just to whet your appetite:: import ldap from django_auth_ldap.config import LDAPSearch, GroupOfNamesType # Baseline configuration. AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com" AUTH_LDAP_BIND_DN = "cn=django-agent,dc=example,dc=com" AUTH_LDAP_BIND_PASSWORD = "phlebotinum" AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)") # or perhaps: # AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com" # Set up the basic group parameters. AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=django,ou=groups,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" ) AUTH_LDAP_GROUP_TYPE = GroupOfNamesType() # Simple group restrictions AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=django,ou=groups,dc=example,dc=com" AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=django,ou=groups,dc=example,dc=com" # Populate the Django user from the LDAP directory. AUTH_LDAP_USER_ATTR_MAP = { "first_name": "givenName", "last_name": "sn", "email": "mail" } AUTH_LDAP_PROFILE_ATTR_MAP = { "employee_number": "employeeNumber" } AUTH_LDAP_USER_FLAGS_BY_GROUP = { "is_active": "cn=active,ou=django,ou=groups,dc=example,dc=com", "is_staff": "cn=staff,ou=django,ou=groups,dc=example,dc=com", "is_superuser": "cn=superuser,ou=django,ou=groups,dc=example,dc=com" } AUTH_LDAP_PROFILE_FLAGS_BY_GROUP = { "is_awesome": "cn=awesome,ou=django,ou=groups,dc=example,dc=com", } # Use LDAP group membership to calculate group permissions. AUTH_LDAP_FIND_GROUP_PERMS = True # Cache group memberships for an hour to minimize LDAP traffic AUTH_LDAP_CACHE_GROUPS = True AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 # Keep ModelBackend around for per-user permissions and maybe a local # superuser. AUTHENTICATION_BACKENDS = ( 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ) Keywords: django,ldap,authentication,auth Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: BSD License Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP Classifier: Topic :: Software Development :: Libraries :: Python Modules django-auth-ldap-1.1.8/django_auth_ldap.egg-info/requires.txt 0000644 0000766 0000024 00000000031 12273247060 025002 0 ustar psagers staff 0000000 0000000 django python-ldap >= 2.0 django-auth-ldap-1.1.8/django_auth_ldap.egg-info/SOURCES.txt 0000644 0000766 0000024 00000004214 12273247061 024276 0 ustar psagers staff 0000000 0000000 CHANGES LICENSE MANIFEST.in README setup.py django_auth_ldap/__init__.py django_auth_ldap/backend.py django_auth_ldap/config.py django_auth_ldap/dn.py django_auth_ldap/models.py django_auth_ldap/tests.py django_auth_ldap.egg-info/PKG-INFO django_auth_ldap.egg-info/SOURCES.txt django_auth_ldap.egg-info/dependency_links.txt django_auth_ldap.egg-info/requires.txt django_auth_ldap.egg-info/top_level.txt docs/Makefile docs/_templates/globaltoc.html docs/archive/versions/1.0.19/.buildinfo docs/archive/versions/1.0.19/genindex.html docs/archive/versions/1.0.19/index.html docs/archive/versions/1.0.19/objects.inv docs/archive/versions/1.0.19/py-modindex.html docs/archive/versions/1.0.19/search.html docs/archive/versions/1.0.19/searchindex.js docs/archive/versions/1.0.19/_sources/index.txt docs/archive/versions/1.0.19/_static/ajax-loader.gif docs/archive/versions/1.0.19/_static/basic.css docs/archive/versions/1.0.19/_static/comment-bright.png docs/archive/versions/1.0.19/_static/comment-close.png docs/archive/versions/1.0.19/_static/comment.png docs/archive/versions/1.0.19/_static/default.css docs/archive/versions/1.0.19/_static/doctools.js docs/archive/versions/1.0.19/_static/down-pressed.png docs/archive/versions/1.0.19/_static/down.png docs/archive/versions/1.0.19/_static/file.png docs/archive/versions/1.0.19/_static/jquery.js docs/archive/versions/1.0.19/_static/minus.png docs/archive/versions/1.0.19/_static/plus.png docs/archive/versions/1.0.19/_static/pygments.css docs/archive/versions/1.0.19/_static/searchtools.js docs/archive/versions/1.0.19/_static/sidebar.js docs/archive/versions/1.0.19/_static/underscore.js docs/archive/versions/1.0.19/_static/up-pressed.png docs/archive/versions/1.0.19/_static/up.png docs/archive/versions/1.0.19/_static/websupport.js docs/ext/daldocs.py docs/source/.spell.utf-8.add docs/source/.spell.utf-8.add.spl docs/source/authentication.rst docs/source/changes.rst docs/source/conf.py docs/source/example.rst docs/source/groups.rst docs/source/index.rst docs/source/install.rst docs/source/logging.rst docs/source/multiconfig.rst docs/source/performance.rst docs/source/permissions.rst docs/source/reference.rst docs/source/users.rst django-auth-ldap-1.1.8/django_auth_ldap.egg-info/top_level.txt 0000644 0000766 0000024 00000000021 12273247060 025133 0 ustar psagers staff 0000000 0000000 django_auth_ldap django-auth-ldap-1.1.8/docs/ 0000755 0000766 0000024 00000000000 12273247061 016364 5 ustar psagers staff 0000000 0000000 django-auth-ldap-1.1.8/docs/_templates/ 0000755 0000766 0000024 00000000000 12273247061 020521 5 ustar psagers staff 0000000 0000000 django-auth-ldap-1.1.8/docs/_templates/globaltoc.html 0000644 0000766 0000024 00000000511 12242245151 023344 0 ustar psagers staff 0000000 0000000 {# basic/globaltoc.html ~~~~~~~~~~~~~~~~~~~~ Sphinx sidebar template: global table of contents. :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. #}