mirror of
https://github.com/ansible/awx.git
synced 2026-05-13 12:27:37 -02:30
AC-156. Add dependencies for LDAP support.
This commit is contained in:
@@ -5,6 +5,7 @@ amqp-1.0.13 (amqp/*)
|
|||||||
anyjson-0.3.3 (anyjson/*)
|
anyjson-0.3.3 (anyjson/*)
|
||||||
billiard-2.7.3.32 (billiard/*, funtests/*, excluded _billiard.so)
|
billiard-2.7.3.32 (billiard/*, funtests/*, excluded _billiard.so)
|
||||||
celery-3.0.22 (celery/*, excluded bin/celery* and bin/camqadm)
|
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-celery-3.0.21 (djcelery/*, excluded bin/djcelerymon)
|
||||||
django-extensions-1.2.0 (django_extensions/*)
|
django-extensions-1.2.0 (django_extensions/*)
|
||||||
django-jsonfield-0.9.10 (jsonfield/*)
|
django-jsonfield-0.9.10 (jsonfield/*)
|
||||||
|
|||||||
2
awx/lib/site-packages/django_auth_ldap/__init__.py
Normal file
2
awx/lib/site-packages/django_auth_ldap/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
version = (1, 1, 4)
|
||||||
|
version_string = "1.1.4"
|
||||||
859
awx/lib/site-packages/django_auth_ldap/backend.py
Normal file
859
awx/lib/site-packages/django_auth_ldap/backend.py
Normal file
@@ -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)
|
||||||
522
awx/lib/site-packages/django_auth_ldap/config.py
Normal file
522
awx/lib/site-packages/django_auth_ldap/config.py
Normal file
@@ -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)
|
||||||
32
awx/lib/site-packages/django_auth_ldap/dn.py
Normal file
32
awx/lib/site-packages/django_auth_ldap/dn.py
Normal file
@@ -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
|
||||||
31
awx/lib/site-packages/django_auth_ldap/models.py
Normal file
31
awx/lib/site-packages/django_auth_ldap/models.py
Normal file
@@ -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)
|
||||||
1401
awx/lib/site-packages/django_auth_ldap/tests.py
Normal file
1401
awx/lib/site-packages/django_auth_ldap/tests.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
|||||||
Django>=1.4
|
Django>=1.4
|
||||||
|
|
||||||
# The following packages are now bundled with AWX (awx/lib/site-packages):
|
# The following packages are now bundled with AWX (awx/lib/site-packages):
|
||||||
|
#django-auth-ldap
|
||||||
#django-celery
|
#django-celery
|
||||||
#django-extensions
|
#django-extensions
|
||||||
#django-jsonfield
|
#django-jsonfield
|
||||||
@@ -27,6 +28,7 @@ ipython
|
|||||||
# package manager, or pip if you're running inside a virtualenv.
|
# package manager, or pip if you're running inside a virtualenv.
|
||||||
# - ansible (via yum, pip or source checkout)
|
# - ansible (via yum, pip or source checkout)
|
||||||
# - psycopg2 (via "yum install python-psycopg2")
|
# - 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";
|
# - coverage (if you want to check test coverage, via "pip install coverage";
|
||||||
# the default python-coverage package is old.)
|
# the default python-coverage package is old.)
|
||||||
# - readline (for using the ipython interactive shell)
|
# - readline (for using the ipython interactive shell)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Django-1.5.2.tar.gz
|
|||||||
#celery-3.0.22.tar.gz
|
#celery-3.0.22.tar.gz
|
||||||
#pytz-2013b.tar.gz
|
#pytz-2013b.tar.gz
|
||||||
# Remaining dev/prod packages:
|
# Remaining dev/prod packages:
|
||||||
|
#django-auth-ldap-1.1.4.tar.gz
|
||||||
#django-celery-3.0.21.tar.gz
|
#django-celery-3.0.21.tar.gz
|
||||||
#django-extensions-1.2.0.tar.gz
|
#django-extensions-1.2.0.tar.gz
|
||||||
#django-jsonfield-0.9.10.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.
|
# package manager, or pip if you're running inside a virtualenv.
|
||||||
# - ansible (via yum, pip or source checkout)
|
# - ansible (via yum, pip or source checkout)
|
||||||
# - psycopg2 (via "yum install python-psycopg2")
|
# - 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
|
# - coverage-3.6.tar.gz (if you want to check test coverage; the default
|
||||||
# python-coverage package is old.)
|
# python-coverage package is old.)
|
||||||
# - readline-6.2.4.1.tar.gz (for using the ipython interactive shell)
|
# - readline-6.2.4.1.tar.gz (for using the ipython interactive shell)
|
||||||
|
|||||||
BIN
requirements/django-auth-ldap-1.1.4.tar.gz
Normal file
BIN
requirements/django-auth-ldap-1.1.4.tar.gz
Normal file
Binary file not shown.
@@ -4,6 +4,7 @@
|
|||||||
Django>=1.4
|
Django>=1.4
|
||||||
|
|
||||||
# The following packages are now bundled with AWX (awx/lib/site-packages):
|
# The following packages are now bundled with AWX (awx/lib/site-packages):
|
||||||
|
#django-auth-ldap
|
||||||
#django-celery
|
#django-celery
|
||||||
#django-extensions
|
#django-extensions
|
||||||
#django-jsonfield
|
#django-jsonfield
|
||||||
@@ -19,3 +20,4 @@ Django>=1.4
|
|||||||
# package manager, or pip if you're running inside a virtualenv.
|
# package manager, or pip if you're running inside a virtualenv.
|
||||||
# - ansible (via yum, pip or source checkout)
|
# - ansible (via yum, pip or source checkout)
|
||||||
# - psycopg2 (via "yum install python-psycopg2")
|
# - psycopg2 (via "yum install python-psycopg2")
|
||||||
|
# - python-ldap (via "yum install python-ldap")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Django-1.5.2.tar.gz
|
|||||||
#celery-3.0.22.tar.gz
|
#celery-3.0.22.tar.gz
|
||||||
#pytz-2013b.tar.gz
|
#pytz-2013b.tar.gz
|
||||||
# Remaining dev/prod packages:
|
# Remaining dev/prod packages:
|
||||||
|
#django-auth-ldap-1.1.4.tar.gz
|
||||||
#django-celery-3.0.21.tar.gz
|
#django-celery-3.0.21.tar.gz
|
||||||
#django-extensions-1.2.0.tar.gz
|
#django-extensions-1.2.0.tar.gz
|
||||||
#django-jsonfield-0.9.10.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.
|
# package manager, or pip if you're running inside a virtualenv.
|
||||||
# - ansible (via yum, pip or source checkout)
|
# - ansible (via yum, pip or source checkout)
|
||||||
# - psycopg2 (via "yum install python-psycopg2")
|
# - psycopg2 (via "yum install python-psycopg2")
|
||||||
|
# - python-ldap (via "yum install python-ldap")
|
||||||
|
|||||||
Reference in New Issue
Block a user