diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 4fdc097c33..57152aaf16 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -355,6 +355,40 @@ AUTH_LDAP_CONNECTION_OPTIONS = { ldap.OPT_NETWORK_TIMEOUT: 30 } +# LDAP Backend settings +_AUTH_LDAP_SETTINGS_BASE = { + "AUTH_LDAP_SERVER_URI": "", + "AUTH_LDAP_BIND_DN": "", + "AUTH_LDAP_BIND_PASSWORD": "", + "AUTH_LDAP_START_TLS": False, + "AUTH_LDAP_USER_SEARCH": [], + "AUTH_LDAP_USER_DN_TEMPLATE": None, + "AUTH_LDAP_USER_ATTR_MAP": {}, + "AUTH_LDAP_GROUP_SEARCH": [], + "AUTH_LDAP_GROUP_TYPE": None, + "AUTH_LDAP_GROUP_TYPE_PARAMS": {}, + "AUTH_LDAP_REQUIRE_GROUP": None, + "AUTH_LDAP_DENY_GROUP": None, + "AUTH_LDAP_USER_FLAGS_BY_GROUP": {}, + "AUTH_LDAP_ORGANIZATION_MAP": {}, + "AUTH_LDAP_TEAM_MAP": {}, +} + + +def generate_ldap_backend(kv, count=None): + num = '' + if count is not None: + num = '{}_'.format(count) + for k,v in kv.iteritems(): + new_k = k.replace('AUTH_LDAP_', 'AUTH_LDAP_{}'.format(num)) + setattr(sys.modules[__name__], new_k, v) + + +generate_ldap_backend(_AUTH_LDAP_SETTINGS_BASE) +for i in xrange(1, 5): + generate_ldap_backend(_AUTH_LDAP_SETTINGS_BASE, i) + + # Radius server settings (default to empty string to skip using Radius auth). # Note: These settings may be overridden by database settings. RADIUS_SERVER = '' diff --git a/awx/sso/backends.py b/awx/sso/backends.py index 03bd7132da..4b20ec165f 100644 --- a/awx/sso/backends.py +++ b/awx/sso/backends.py @@ -42,6 +42,7 @@ class LDAPSettings(BaseLDAPSettings): defaults = dict(BaseLDAPSettings.defaults.items() + { 'ORGANIZATION_MAP': {}, 'TEAM_MAP': {}, + 'GROUP_TYPE_PARAMS': {}, }.items()) def __init__(self, prefix='AUTH_LDAP_', defaults={}): diff --git a/awx/sso/conf.py b/awx/sso/conf.py index e2c15c96fa..65513e7882 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -295,6 +295,27 @@ def _register_ldap(append=None): category_slug='ldap', feature_required='ldap', default='MemberDNGroupType', + depends_on=['AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str)], + ) + + register( + 'AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str), + field_class=fields.LDAPGroupTypeParamsField, + label=_('LDAP Group Type'), + help_text=_('Parameters to send the chosen group type.'), + category=_('LDAP'), + category_slug='ldap', + default=collections.OrderedDict([ + #('member_attr', 'member'), + ('name_attr', 'cn'), + ]), + placeholder=collections.OrderedDict([ + ('ldap_group_user_attr', 'legacyuid'), + ('member_attr', 'member'), + ('name_attr', 'cn'), + ]), + feature_required='ldap', + depends_on=['AUTH_LDAP{}_GROUP_TYPE'.format(append_str)], ) register( diff --git a/awx/sso/fields.py b/awx/sso/fields.py index b1868975e1..997a311823 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -1,5 +1,6 @@ # Python LDAP import ldap +import awx # Django from django.utils.translation import ugettext_lazy as _ @@ -9,6 +10,9 @@ from django.core.exceptions import ValidationError import django_auth_ldap.config from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion +# This must be imported so get_subclasses picks it up +from awx.sso.ldap_group_types import PosixUIDGroupType # noqa + # Tower from awx.conf import fields from awx.conf.fields import * # noqa @@ -335,19 +339,48 @@ class LDAPGroupTypeField(fields.ChoiceField): def to_representation(self, value): if not value: - return '' + return 'MemberDNGroupType' if not isinstance(value, django_auth_ldap.config.LDAPGroupType): self.fail('type_error', input_type=type(value)) return value.__class__.__name__ def to_internal_value(self, data): + def find_class_in_modules(class_name): + module_search_space = [django_auth_ldap.config, awx.sso.ldap_group_types] + for m in module_search_space: + cls = getattr(m, class_name, None) + if cls: + return cls + return None + data = super(LDAPGroupTypeField, self).to_internal_value(data) if not data: return None + + from django.conf import settings + params = getattr(settings, iter(self.depends_on).next(), None) or {} + cls = find_class_in_modules(data) + if not cls: + return None + + # Per-group type parameter validation and handling here + + # Backwords compatability. Before AUTH_LDAP_GROUP_TYPE_PARAMS existed + # MemberDNGroupType was the only group type, of the underlying lib, that + # took a parameter. + params_sanitized = dict() if data.endswith('MemberDNGroupType'): - return getattr(django_auth_ldap.config, data)(member_attr='member') - else: - return getattr(django_auth_ldap.config, data)() + params.setdefault('member_attr', 'member') + params_sanitized['member_attr'] = params['member_attr'] + elif data.endswith('PosixUIDGroupType'): + params.setdefault('ldap_group_user_attr', 'uid') + params_sanitized['ldap_group_user_attr'] = params['ldap_group_user_attr'] + + return cls(**params_sanitized) + + +class LDAPGroupTypeParamsField(fields.DictField): + pass class LDAPUserFlagsField(fields.DictField): diff --git a/awx/sso/ldap_group_types.py b/awx/sso/ldap_group_types.py new file mode 100644 index 0000000000..4f8402eaa7 --- /dev/null +++ b/awx/sso/ldap_group_types.py @@ -0,0 +1,76 @@ +# Copyright (c) 2017 Ansible by Red Hat +# All Rights Reserved. + +# Python +import ldap + +# Django +from django.utils.encoding import force_str + +# 3rd party +from django_auth_ldap.config import LDAPGroupType + + +class PosixUIDGroupType(LDAPGroupType): + + def __init__(self, ldap_group_user_attr, *args, **kwargs): + super(PosixUIDGroupType, self).__init__(*args, **kwargs) + + self.ldap_group_user_attr = ldap_group_user_attr + + """ + An LDAPGroupType subclass that handles non-standard DS. + """ + 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[self.ldap_group_user_attr][0] + + if 'gidNumber' in ldap_user.attrs: + user_gid = ldap_user.attrs['gidNumber'][0] + filterstr = u'(|(gidNumber=%s)(memberUid=%s))' % ( + self.ldap.filter.escape_filter_chars(user_gid), + self.ldap.filter.escape_filter_chars(user_uid) + ) + else: + filterstr = u'(memberUid=%s)' % ( + self.ldap.filter.escape_filter_chars(user_uid), + ) + + search = group_search.search_with_additional_term_string(filterstr) + groups = search.execute(ldap_user.connection) + except (KeyError, IndexError): + pass + + return groups + + def is_member(self, ldap_user, group_dn): + """ + Returns True if the group is the user's primary group or if the user is + listed in the group's memberUid attribute. + """ + is_member = False + try: + user_uid = ldap_user.attrs[self.ldap_group_user_attr][0] + + try: + is_member = ldap_user.connection.compare_s(force_str(group_dn), 'memberUid', force_str(user_uid)) + except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): + is_member = False + + if not is_member: + try: + user_gid = ldap_user.attrs['gidNumber'][0] + is_member = ldap_user.connection.compare_s(force_str(group_dn), 'gidNumber', force_str(user_gid)) + except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): + is_member = False + except (KeyError, IndexError): + is_member = False + + return is_member +