mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03:30
AC-156. Add dependencies for LDAP support.
This commit is contained in:
parent
8cdbaa83b2
commit
1763d373eb
@ -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/*)
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
@ -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)
|
||||
|
||||
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
|
||||
|
||||
# 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")
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user