diff --git a/awx/lib/site-packages/README b/awx/lib/site-packages/README index aa3205363d..137586591a 100644 --- a/awx/lib/site-packages/README +++ b/awx/lib/site-packages/README @@ -5,6 +5,7 @@ amqp-1.0.13 (amqp/*) anyjson-0.3.3 (anyjson/*) billiard-2.7.3.32 (billiard/*, funtests/*, excluded _billiard.so) celery-3.0.22 (celery/*, excluded bin/celery* and bin/camqadm) +django-auth-ldap-1.1.4 (django_auth_ldap/*) django-celery-3.0.21 (djcelery/*, excluded bin/djcelerymon) django-extensions-1.2.0 (django_extensions/*) django-jsonfield-0.9.10 (jsonfield/*) diff --git a/awx/lib/site-packages/django_auth_ldap/__init__.py b/awx/lib/site-packages/django_auth_ldap/__init__.py new file mode 100644 index 0000000000..88ef52114c --- /dev/null +++ b/awx/lib/site-packages/django_auth_ldap/__init__.py @@ -0,0 +1,2 @@ +version = (1, 1, 4) +version_string = "1.1.4" diff --git a/awx/lib/site-packages/django_auth_ldap/backend.py b/awx/lib/site-packages/django_auth_ldap/backend.py new file mode 100644 index 0000000000..3dd15a51ae --- /dev/null +++ b/awx/lib/site-packages/django_auth_ldap/backend.py @@ -0,0 +1,859 @@ +# 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. +""" + +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback + +import sys +import traceback +import pprint +import copy + +import django.db +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 + 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) + + # + # 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 = 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 = 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 self.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 self.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 self.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 self.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, self.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 = self.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_dn in self.settings.USER_FLAGS_BY_GROUP.iteritems(): + value = self._get_groups().is_member_of(group_dn) + 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_dn in self.settings.PROFILE_FLAGS_BY_GROUP.iteritems(): + value = self._get_groups().is_member_of(group_dn) + 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 + ).values_list('content_type__app_label', 'codename' + ).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: + self._connection = self.ldap.initialize(self.settings.SERVER_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) diff --git a/awx/lib/site-packages/django_auth_ldap/config.py b/awx/lib/site-packages/django_auth_ldap/config.py new file mode 100644 index 0000000000..d01b7d09f7 --- /dev/null +++ b/awx/lib/site-packages/django_auth_ldap/config.py @@ -0,0 +1,522 @@ +# 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. +""" + +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback + +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 + 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 self.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 self.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 != self.ldap.RES_SEARCH_RESULT: + results = [] + except self.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 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] + 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) + ) + + 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] + user_gid = ldap_user.attrs['gidNumber'][0] + + try: + is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'memberUid', user_uid.encode('utf-8')) + except self.ldap.NO_SUCH_ATTRIBUTE: + is_member = False + + if not is_member: + try: + is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'gidNumber', user_gid.encode('utf-8')) + except self.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 self.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) diff --git a/awx/lib/site-packages/django_auth_ldap/dn.py b/awx/lib/site-packages/django_auth_ldap/dn.py new file mode 100644 index 0000000000..78271abbd4 --- /dev/null +++ b/awx/lib/site-packages/django_auth_ldap/dn.py @@ -0,0 +1,32 @@ +# 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 diff --git a/awx/lib/site-packages/django_auth_ldap/models.py b/awx/lib/site-packages/django_auth_ldap/models.py new file mode 100644 index 0000000000..b48d7a38ca --- /dev/null +++ b/awx/lib/site-packages/django_auth_ldap/models.py @@ -0,0 +1,31 @@ +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) diff --git a/awx/lib/site-packages/django_auth_ldap/tests.py b/awx/lib/site-packages/django_auth_ldap/tests.py new file mode 100644 index 0000000000..0bdbff5957 --- /dev/null +++ b/awx/lib/site-packages/django_auth_ldap/tests.py @@ -0,0 +1,1401 @@ +# 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. + +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback + +from collections import defaultdict +from copy import deepcopy +import logging +import pickle +import sys + +from django.conf import settings +import django.db.models.signals +from django.contrib.auth.models import User, Permission, Group +from django.test import TestCase + +try: + from django.test.utils import override_settings +except ImportError: + override_settings = lambda *args, **kwargs: (lambda v: v) + +from django_auth_ldap.models import TestUser, TestProfile +from django_auth_ldap import backend +from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion +from django_auth_ldap.config import PosixGroupType, MemberDNGroupType, NestedMemberDNGroupType +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 MockLDAP(object): + """ + This is a stand-in for the python-ldap module; it serves as both the ldap + module and the LDAPObject class. While it's temping to add some real LDAP + capabilities here, this is designed to remain as simple as possible, so as + to minimize the risk of creating bogus unit tests through a buggy test + harness. + + Simple operations can be simulated, but for nontrivial searches, the client + will have to seed the mock object with return values for expected API calls. + This may sound like cheating, but it's really no more so than a simulated + LDAP server. The fact is we can not require python-ldap to be installed in + order to run the unit tests, so all we can do is verify that LDAPBackend is + calling the APIs that we expect. + + set_return_value takes the name of an API, a tuple of arguments, and a + return value. Every time an API is called, it looks for a predetermined + return value based on the arguments received. If it finds one, then it + returns it, or raises it if it's an Exception. If it doesn't find one, then + it tries to satisfy the request internally. If it can't, it raises a + PresetReturnRequiredError. + + At any time, the client may call ldap_methods_called_with_arguments() or + ldap_methods_called() to get a record of all of the LDAP API calls that have + been made, with or without arguments. + """ + class PresetReturnRequiredError(Exception): + pass + + SCOPE_BASE = 0 + SCOPE_ONELEVEL = 1 + SCOPE_SUBTREE = 2 + + RES_SEARCH_RESULT = 101 + + class LDAPError(Exception): + pass + + class INVALID_CREDENTIALS(LDAPError): + pass + + class NO_SUCH_OBJECT(LDAPError): + pass + + class NO_SUCH_ATTRIBUTE(LDAPError): + pass + + # + # Submodules + # + class dn(object): + def escape_dn_chars(s): + return s + escape_dn_chars = staticmethod(escape_dn_chars) + + class filter(object): + def escape_filter_chars(s): + return s + escape_filter_chars = staticmethod(escape_filter_chars) + + class cidict(object): + class cidict(dict): + pass + + def __init__(self, directory): + """ + directory is a complex structure with the entire contents of the + mock LDAP directory. directory must be a dictionary mapping + distinguished names to dictionaries of attributes. Each attribute + dictionary maps attribute names to lists of values. e.g.: + + { + "uid=alice,ou=users,dc=example,dc=com": + { + "uid": ["alice"], + "userPassword": ["secret"], + }, + } + """ + self.directory = self.cidict.cidict(directory) + + self.reset() + + def reset(self): + """ + Resets our recorded API calls and queued return values as well as + miscellaneous configuration options. + """ + self.calls = [] + self.return_value_maps = defaultdict(lambda: {}) + self.async_results = [] + self.options = {} + self.tls_enabled = False + + def set_return_value(self, api_name, arguments, value): + """ + Stores a preset return value for a given API with a given set of + arguments. + """ + self.return_value_maps[api_name][arguments] = value + + def ldap_methods_called_with_arguments(self): + """ + Returns a list of 2-tuples, one for each API call made since the last + reset. Each tuple contains the name of the API and a dictionary of + arguments. Argument defaults are included. + """ + return self.calls + + def ldap_methods_called(self): + """ + Returns the list of API names called. + """ + return [call[0] for call in self.calls] + + # + # Begin LDAP methods + # + + def set_option(self, option, invalue): + self._record_call('set_option', { + 'option': option, + 'invalue': invalue + }) + + self.options[option] = invalue + + def initialize(self, uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None): + self._record_call('initialize', { + 'uri': uri, + 'trace_level': trace_level, + 'trace_file': trace_file, + 'trace_stack_limit': trace_stack_limit + }) + + value = self._get_return_value('initialize', + (uri, trace_level, trace_file, trace_stack_limit)) + if value is None: + value = self + + return value + + def simple_bind_s(self, who='', cred=''): + self._record_call('simple_bind_s', { + 'who': who, + 'cred': cred + }) + + value = self._get_return_value('simple_bind_s', (who, cred)) + if value is None: + value = self._simple_bind_s(who, cred) + + return value + + def search(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + self._record_call('search', { + 'base': base, + 'scope': scope, + 'filterstr': filterstr, + 'attrlist': attrlist, + 'attrsonly': attrsonly + }) + + value = self._get_return_value('search_s', + (base, scope, filterstr, attrlist, attrsonly)) + if value is None: + value = self._search_s(base, scope, filterstr, attrlist, attrsonly) + + return self._add_async_result(value) + + def result(self, msgid, all=1, timeout=None): + self._record_call('result', { + 'msgid': msgid, + 'all': all, + 'timeout': timeout, + }) + + return self.RES_SEARCH_RESULT, self._pop_async_result(msgid) + + def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + self._record_call('search_s', { + 'base': base, + 'scope': scope, + 'filterstr': filterstr, + 'attrlist': attrlist, + 'attrsonly': attrsonly + }) + + value = self._get_return_value('search_s', + (base, scope, filterstr, attrlist, attrsonly)) + if value is None: + value = self._search_s(base, scope, filterstr, attrlist, attrsonly) + + return value + + def start_tls_s(self): + self.tls_enabled = True + + def compare_s(self, dn, attr, value): + self._record_call('compare_s', { + 'dn': dn, + 'attr': attr, + 'value': value + }) + + result = self._get_return_value('compare_s', (dn, attr, value)) + if result is None: + result = self._compare_s(dn, attr, value) + + # print "compare_s('%s', '%s', '%s'): %d" % (dn, attr, value, result) + + return result + + # + # Internal implementations + # + + def _simple_bind_s(self, who='', cred=''): + success = False + + if(who == '' and cred == ''): + success = True + elif self._compare_s(who.lower(), 'userPassword', cred): + success = True + + if success: + return (97, []) # python-ldap returns this; I don't know what it means + else: + raise self.INVALID_CREDENTIALS('%s:%s' % (who, cred)) + + def _compare_s(self, dn, attr, value): + if dn not in self.directory: + raise self.NO_SUCH_OBJECT + + if attr not in self.directory[dn]: + raise self.NO_SUCH_ATTRIBUTE + + return (value in self.directory[dn][attr]) and 1 or 0 + + def _search_s(self, base, scope, filterstr, attrlist, attrsonly): + """ + We can do a SCOPE_BASE search with the default filter. Beyond that, + you're on your own. + """ + if scope != self.SCOPE_BASE: + raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' % + (base, scope, filterstr, attrlist, attrsonly)) + + if filterstr != '(objectClass=*)': + raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' % + (base, scope, filterstr, attrlist, attrsonly)) + + attrs = self.directory.get(base) + if attrs is None: + raise self.NO_SUCH_OBJECT() + + return [(base, attrs)] + + def _add_async_result(self, value): + self.async_results.append(value) + + return len(self.async_results) - 1 + + def _pop_async_result(self, msgid): + if msgid in xrange(len(self.async_results)): + value = self.async_results[msgid] + self.async_results[msgid] = None + else: + value = None + + return value + + # + # Utils + # + + def _record_call(self, api_name, arguments): + self.calls.append((api_name, arguments)) + + def _get_return_value(self, api_name, arguments): + try: + value = self.return_value_maps[api_name][arguments] + except KeyError: + value = None + + if isinstance(value, Exception): + raise value + + return value + + +class LDAPTest(TestCase): + # Following are the objecgs in our mock LDAP directory + 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"], + }) + 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"], + }) + + # groupOfUniqueName groups + 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"] + }) + + # 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"] + }) + + mock_ldap = MockLDAP({ + alice[0]: alice[1], + bob[0]: bob[1], + dressler[0]: dressler[1], + nobody[0]: nobody[1], + active_px[0]: active_px[1], + staff_px[0]: staff_px[1], + superuser_px[0]: superuser_px[1], + active_gon[0]: active_gon[1], + staff_gon[0]: staff_gon[1], + superuser_gon[0]: superuser_gon[1], + parent_gon[0]: parent_gon[1], + nested_gon[0]: nested_gon[1], + circular_gon[0]: circular_gon[1], + }) + + logging_configured = False + + def configure_logger(cls): + if not cls.logging_configured: + 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) + + cls.logging_configured = True + configure_logger = classmethod(configure_logger) + + def setUp(self): + self.configure_logger() + + self.ldap = _LDAPConfig.ldap = self.mock_ldap + + self.backend = backend.LDAPBackend() + self.backend.ldap # Force global configuration + + self.mock_ldap.reset() + + def tearDown(self): + pass + + 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.mock_ldap.options, {'opt1': 'value1'}) + + 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.assert_(not user.has_usable_password()) + self.assertEqual(user.username, 'alice') + self.assertEqual(User.objects.count(), user_count + 1) + self.assertEqual(self.mock_ldap.ldap_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.assert_(not user.has_usable_password()) + self.assertEqual(user.username, 'alice') + self.assertEqual(User.objects.count(), user_count + 1) + self.assertEqual(self.mock_ldap.ldap_methods_called(), + ['initialize', 'simple_bind_s']) + + def test_deepcopy(self): + self._init_settings( + USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' + ) + + user = self.backend.authenticate(username='Alice', password='password') + user = deepcopy(user) + + @override_settings(AUTH_USER_MODEL='django_auth_ldap.TestUser') + def test_auth_custom_user(self): + self._init_settings( + USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', + ) + + user = self.backend.authenticate(username='Alice', password='password') + + self.assert_(isinstance(user, TestUser)) + + @override_settings(AUTH_USER_MODEL='django_auth_ldap.TestUser') + def test_get_custom_user(self): + self._init_settings( + USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', + ) + + user = self.backend.authenticate(username='Alice', password='password') + user = self.backend.get_user(user.id) + + self.assert_(isinstance(user, TestUser)) + + def test_new_user_whitespace(self): + self._init_settings( + USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' + ) + user_count = User.objects.count() + + user = self.backend.authenticate(username=' alice', password='password') + user = self.backend.authenticate(username='alice ', password='password') + + self.assert_(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.assert_(user is None) + self.assertEqual(User.objects.count(), user_count) + self.assertEqual(self.mock_ldap.ldap_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.assert_(user is None) + self.assertEqual(User.objects.count(), user_count) + self.assertEqual(self.mock_ldap.ldap_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.assert_(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", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' + ) + ) + self.mock_ldap.set_return_value('search_s', + ("ou=people,o=test", 2, "(uid=Alice)", None, 0), [self.alice]) + User.objects.create(username='alice') + + user = self.backend.authenticate(username='Alice', password='password') + + self.assert_(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", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' + ) + ) + self.mock_ldap.set_return_value('search_s', + ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice]) + user_count = User.objects.count() + + user = self.backend.authenticate(username='alice', password='password') + + self.assert_(user is not None) + self.assertEqual(User.objects.count(), user_count + 1) + self.assertEqual(self.mock_ldap.ldap_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", self.mock_ldap.SCOPE_SUBTREE, '(cn=%(user)s)' + ) + ) + self.mock_ldap.set_return_value('search_s', + ("ou=people,o=test", 2, "(cn=alice)", None, 0), []) + + user = self.backend.authenticate(username='alice', password='password') + + self.assert_(user is None) + self.assertEqual(self.mock_ldap.ldap_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", self.mock_ldap.SCOPE_SUBTREE, '(uid=*)' + ) + ) + self.mock_ldap.set_return_value('search_s', + ("ou=people,o=test", 2, "(uid=*)", None, 0), [self.alice, self.bob]) + + user = self.backend.authenticate(username='alice', password='password') + + self.assert_(user is None) + self.assertEqual(self.mock_ldap.ldap_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", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' + ) + ) + self.mock_ldap.set_return_value('search_s', + ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice]) + + user = self.backend.authenticate(username='alice', password='bogus') + + self.assert_(user is None) + self.assertEqual(self.mock_ldap.ldap_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", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' + ) + ) + self.mock_ldap.set_return_value('search_s', + ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice]) + + user = self.backend.authenticate(username='alice', password='password') + + self.assert_(user is not None) + self.assert_(user.ldap_user is not None) + self.assertEqual(user.ldap_user.dn, self.alice[0]) + self.assertEqual(user.ldap_user.attrs, self.alice[1]) + self.assertEqual(self.mock_ldap.ldap_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", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' + ) + ) + + user = self.backend.authenticate(username='alice', password='password') + + self.assert_(user is None) + self.assertEqual(self.mock_ldap.ldap_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.assert_(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.assert_(isinstance(user.ldap_user.attrs, self.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.mock_ldap.ldap_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.mock_ldap.ldap_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.assert_('user' in kwargs and 'ldap_user' in kwargs) + kwargs['user'].populate_user_handled = True + backend.populate_user.connect(handle_populate_user) + + user = self.backend.authenticate(username='alice', password='password') + + self.assert_(user.populate_user_handled) + + def test_signal_populate_user_profile(self): + settings.AUTH_PROFILE_MODULE = 'django_auth_ldap.TestProfile' + + self._init_settings( + 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.assert_('profile' in kwargs and 'ldap_user' in kwargs) + kwargs['profile'].populated = True + + django.db.models.signals.post_save.connect(handle_user_saved, sender=User) + backend.populate_user_profile.connect(handle_populate_user_profile) + + user = self.backend.authenticate(username='alice', password='password') + + self.assert_(user.get_profile().populated) + + def test_no_update_existing(self): + self._init_settings( + 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', self.mock_ldap.SCOPE_SUBTREE), + 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.assert_(alice is not None) + self.assert_(bob is None) + self.assertEqual(self.mock_ldap.ldap_methods_called(), + ['initialize', 'simple_bind_s', 'simple_bind_s', 'compare_s', 'initialize', 'simple_bind_s', 'simple_bind_s', 'compare_s']) + + 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', self.mock_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.assert_(alice is None) + self.assert_(bob is not None) + self.assertEqual(self.mock_ldap.ldap_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', self.mock_ldap.SCOPE_SUBTREE), + GROUP_TYPE=MemberDNGroupType(member_attr='member'), + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0), + [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon] + ) + + 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', self.mock_ldap.SCOPE_SUBTREE), + GROUP_TYPE=MemberDNGroupType(member_attr='member'), + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0), + [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon] + ) + + 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', self.mock_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=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.assert_(alice.is_active) + self.assert_(alice.is_staff) + self.assert_(alice.is_superuser) + self.assert_(not bob.is_active) + self.assert_(not bob.is_staff) + self.assert_(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', self.mock_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.assert_(alice.is_active) + self.assert_(alice.is_staff) + self.assert_(alice.is_superuser) + self.assert_(not bob.is_active) + self.assert_(not bob.is_staff) + self.assert_(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', self.mock_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", + } + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=alice,ou=people,o=test)))", None, 0), + [self.active_gon, self.nested_gon] + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=active_gon,ou=groups,o=test)(member=cn=nested_gon,ou=groups,o=test)))", None, 0), + [self.parent_gon] + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=parent_gon,ou=groups,o=test)))", None, 0), + [self.circular_gon] + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=circular_gon,ou=groups,o=test)))", None, 0), + [self.nested_gon] + ) + + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=bob,ou=people,o=test)))", None, 0), + [] + ) + + alice = self.backend.authenticate(username='alice', password='password') + bob = self.backend.authenticate(username='bob', password='password') + + self.assert_(alice.is_active) + self.assert_(alice.is_staff) + self.assert_(not bob.is_active) + self.assert_(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', self.mock_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.assert_(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', self.mock_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.assert_(alice.get_profile().is_special) + self.assert_(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', self.mock_ldap.SCOPE_SUBTREE), + GROUP_TYPE=MemberDNGroupType(member_attr='member'), + FIND_GROUP_PERMS=True + ) + self._init_groups() + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0), + [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon] + ) + + 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.assert_(self.backend.has_perm(alice, "auth.add_user")) + self.assert_(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', self.mock_ldap.SCOPE_SUBTREE), + GROUP_TYPE=MemberDNGroupType(member_attr='member'), + FIND_GROUP_PERMS=True + ) + self._init_groups() + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=bob,ou=people,o=test))", None, 0), + [] + ) + + 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.assert_(not self.backend.has_perm(bob, "auth.add_user")) + self.assert_(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', + self.mock_ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" + ), + GROUP_TYPE=PosixGroupType(), + FIND_GROUP_PERMS=True + ) + self._init_groups() + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=posixGroup)(|(gidNumber=1000)(memberUid=alice)))", None, 0), + [self.active_px, self.staff_px, self.superuser_px] + ) + + 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.assert_(self.backend.has_perm(alice, "auth.add_user")) + self.assert_(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', self.mock_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', self.mock_ldap.SCOPE_SUBTREE), + GROUP_TYPE=MemberDNGroupType(member_attr='member'), + FIND_GROUP_PERMS=True, + CACHE_GROUPS=True + ) + self._init_groups() + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0), + [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon] + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=bob,ou=people,o=test))", None, 0), + [] + ) + + 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.mock_ldap.ldap_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', + self.mock_ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" + ), + GROUP_TYPE=PosixGroupType(), + MIRROR_GROUPS=True, + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=posixGroup)(|(gidNumber=1000)(memberUid=alice)))", None, 0), + [self.active_px, self.staff_px, self.superuser_px] + ) + + 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', self.mock_ldap.SCOPE_SUBTREE), + GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'), + MIRROR_GROUPS=True, + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=alice,ou=people,o=test)))", None, 0), + [self.active_gon, self.nested_gon] + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=active_gon,ou=groups,o=test)(member=cn=nested_gon,ou=groups,o=test)))", None, 0), + [self.parent_gon] + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=parent_gon,ou=groups,o=test)))", None, 0), + [self.circular_gon] + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=circular_gon,ou=groups,o=test)))", None, 0), + [self.nested_gon] + ) + + alice = self.backend.authenticate(username='alice', password='password') + + self.assertEqual(Group.objects.count(), 4) + self.assertEqual(set(Group.objects.all().values_list('name', flat=True)), + set(['active_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', self.mock_ldap.SCOPE_SUBTREE), + GROUP_TYPE=MemberDNGroupType(member_attr='member'), + FIND_GROUP_PERMS=True, + AUTHORIZE_ALL_USERS=True + ) + self._init_groups() + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0), + [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon] + ) + + 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.assert_(alice is not None) + self.assertEqual(alice.first_name, u"") + self.assertEqual(alice.last_name, u"") + self.assert_(alice.is_active) + self.assert_(not alice.is_staff) + self.assert_(not alice.is_superuser) + self.assert_(bob is not None) + self.assertEqual(bob.first_name, u"") + self.assertEqual(bob.last_name, u"") + self.assert_(bob.is_active) + self.assert_(not bob.is_staff) + self.assert_(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', self.mock_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.assert_(alice is not None) + self.assertEqual(alice.first_name, u"Alice") + self.assertEqual(alice.last_name, u"Adams") + self.assert_(alice.is_active) + self.assert_(alice.is_staff) + self.assert_(alice.is_superuser) + self.assert_(bob is not None) + self.assertEqual(bob.first_name, u"Robert") + self.assertEqual(bob.last_name, u"Barker") + self.assert_(not bob.is_active) + self.assert_(not bob.is_staff) + self.assert_(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.assert_(not self.mock_ldap.tls_enabled) + self.backend.authenticate(username='alice', password='password') + self.assert_(not self.mock_ldap.tls_enabled) + + def test_start_tls(self): + self._init_settings( + USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', + START_TLS=True, + ) + + self.assert_(not self.mock_ldap.tls_enabled) + self.backend.authenticate(username='alice', password='password') + self.assert_(self.mock_ldap.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", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' + ) + ) + self.mock_ldap.set_return_value('search_s', + ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice, (None, '')]) + + self.backend.authenticate(username='alice', password='password') + + def test_union_search(self): + self._init_settings( + USER_SEARCH=LDAPSearchUnion( + LDAPSearch("ou=groups,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'), + LDAPSearch("ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'), + ) + ) + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(uid=alice)", None, 0), []) + self.mock_ldap.set_return_value('search_s', + ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice]) + + alice = self.backend.authenticate(username='alice', password='password') + + self.assert_(alice is not None) + + self.assertEqual(self.mock_ldap.ldap_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.mock_ldap.ldap_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.mock_ldap.ldap_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', self.mock_ldap.SCOPE_SUBTREE), + GROUP_TYPE=MemberDNGroupType(member_attr='member'), + FIND_GROUP_PERMS=True + ) + self._init_groups() + self.mock_ldap.set_return_value('search_s', + ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0), + [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon] + ) + + 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.assert_(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.assert_(self.backend.has_perm(alice, "auth.add_user")) + self.assert_(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) diff --git a/requirements/dev.txt b/requirements/dev.txt index 5d750fcfa3..54bf44d9b1 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -7,6 +7,7 @@ Django>=1.4 # The following packages are now bundled with AWX (awx/lib/site-packages): + #django-auth-ldap #django-celery #django-extensions #django-jsonfield @@ -27,6 +28,7 @@ ipython # package manager, or pip if you're running inside a virtualenv. # - ansible (via yum, pip or source checkout) # - psycopg2 (via "yum install python-psycopg2") +# - python-ldap (via "yum install python-ldap") # - coverage (if you want to check test coverage, via "pip install coverage"; # the default python-coverage package is old.) # - readline (for using the ipython interactive shell) diff --git a/requirements/dev_local.txt b/requirements/dev_local.txt index b518c47c35..0f0d0e0dbd 100644 --- a/requirements/dev_local.txt +++ b/requirements/dev_local.txt @@ -19,6 +19,7 @@ Django-1.5.2.tar.gz #celery-3.0.22.tar.gz #pytz-2013b.tar.gz # Remaining dev/prod packages: + #django-auth-ldap-1.1.4.tar.gz #django-celery-3.0.21.tar.gz #django-extensions-1.2.0.tar.gz #django-jsonfield-0.9.10.tar.gz @@ -38,6 +39,7 @@ ipython-1.0.0.tar.gz # package manager, or pip if you're running inside a virtualenv. # - ansible (via yum, pip or source checkout) # - psycopg2 (via "yum install python-psycopg2") +# - python-ldap (via "yum install python-ldap") # - coverage-3.6.tar.gz (if you want to check test coverage; the default # python-coverage package is old.) # - readline-6.2.4.1.tar.gz (for using the ipython interactive shell) diff --git a/requirements/django-auth-ldap-1.1.4.tar.gz b/requirements/django-auth-ldap-1.1.4.tar.gz new file mode 100644 index 0000000000..de2f3f6919 Binary files /dev/null and b/requirements/django-auth-ldap-1.1.4.tar.gz differ diff --git a/requirements/prod.txt b/requirements/prod.txt index 73b7f95b2e..11635fbaba 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -4,6 +4,7 @@ Django>=1.4 # The following packages are now bundled with AWX (awx/lib/site-packages): + #django-auth-ldap #django-celery #django-extensions #django-jsonfield @@ -19,3 +20,4 @@ Django>=1.4 # package manager, or pip if you're running inside a virtualenv. # - ansible (via yum, pip or source checkout) # - psycopg2 (via "yum install python-psycopg2") +# - python-ldap (via "yum install python-ldap") diff --git a/requirements/prod_local.txt b/requirements/prod_local.txt index 0694fb1249..f6ad22c556 100644 --- a/requirements/prod_local.txt +++ b/requirements/prod_local.txt @@ -17,6 +17,7 @@ Django-1.5.2.tar.gz #celery-3.0.22.tar.gz #pytz-2013b.tar.gz # Remaining dev/prod packages: + #django-auth-ldap-1.1.4.tar.gz #django-celery-3.0.21.tar.gz #django-extensions-1.2.0.tar.gz #django-jsonfield-0.9.10.tar.gz @@ -31,3 +32,4 @@ Django-1.5.2.tar.gz # package manager, or pip if you're running inside a virtualenv. # - ansible (via yum, pip or source checkout) # - psycopg2 (via "yum install python-psycopg2") +# - python-ldap (via "yum install python-ldap")