diff options
author | Matthew Jones <bsdmatburt@gmail.com> | 2018-03-26 21:29:01 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-26 21:29:01 +0200 |
commit | 96370584062a15271abafab7fc557ac2879aa38c (patch) | |
tree | 1eb5cf1500defaf5b837354188e107243f1c1267 | |
parent | Merge pull request #1679 from mabashian/workflow-node-start-bug (diff) | |
parent | introspect ldap group types for param validation (diff) | |
download | awx-96370584062a15271abafab7fc557ac2879aa38c.tar.xz awx-96370584062a15271abafab7fc557ac2879aa38c.zip |
Merge pull request #1512 from chrismeyersfsu/feature-new_ldap_group_type
add ldap group type like posixGroupType
-rw-r--r-- | awx/conf/settings.py | 7 | ||||
-rw-r--r-- | awx/sso/backends.py | 1 | ||||
-rw-r--r-- | awx/sso/conf.py | 20 | ||||
-rw-r--r-- | awx/sso/fields.py | 89 | ||||
-rw-r--r-- | awx/sso/ldap_group_types.py | 75 | ||||
-rw-r--r-- | awx/sso/tests/unit/test_fields.py | 21 |
6 files changed, 204 insertions, 9 deletions
diff --git a/awx/conf/settings.py b/awx/conf/settings.py index 0af16846b7..4263deaa1d 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -305,7 +305,7 @@ class SettingsWrapper(UserSettingsHolder): settings_to_cache['_awx_conf_preload_expires'] = self._awx_conf_preload_expires self.cache.set_many(settings_to_cache, timeout=SETTING_CACHE_TIMEOUT) - def _get_local(self, name): + def _get_local(self, name, validate=True): self._preload_cache() cache_key = Setting.get_cache_key(name) try: @@ -368,7 +368,10 @@ class SettingsWrapper(UserSettingsHolder): field.run_validators(internal_value) return internal_value else: - return field.run_validation(value) + if validate: + return field.run_validation(value) + else: + return value except Exception: logger.warning( 'The current value "%r" for setting "%s" is invalid.', 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..504b7724d4 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -295,6 +295,26 @@ 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([ + ('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..0e7434f443 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 _ @@ -7,7 +8,13 @@ from django.core.exceptions import ValidationError # Django Auth LDAP import django_auth_ldap.config -from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion +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 @@ -24,6 +31,37 @@ def get_subclasses(cls): yield subclass +def find_class_in_modules(class_name): + ''' + Used to find ldap subclasses by string + ''' + 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 + + +class DependsOnMixin(): + def get_depends_on(self): + """ + Get the value of the dependent field. + First try to find the value in the request. + Then fall back to the raw value from the setting in the DB. + """ + from django.conf import settings + dependent_key = iter(self.depends_on).next() + + if self.context: + request = self.context.get('request', None) + if request and request.data and \ + request.data.get(dependent_key, None): + return request.data.get(dependent_key) + res = settings._get_local(dependent_key, validate=False) + return res + + class AuthenticationBackendsField(fields.StringListField): # Mapping of settings that must be set in order to enable each @@ -322,7 +360,7 @@ class LDAPUserAttrMapField(fields.DictField): return data -class LDAPGroupTypeField(fields.ChoiceField): +class LDAPGroupTypeField(fields.ChoiceField, DependsOnMixin): default_error_messages = { 'type_error': _('Expected an instance of LDAPGroupType but got {input_type} instead.'), @@ -335,7 +373,7 @@ 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__ @@ -344,10 +382,47 @@ class LDAPGroupTypeField(fields.ChoiceField): data = super(LDAPGroupTypeField, self).to_internal_value(data) if not data: return None - if data.endswith('MemberDNGroupType'): - return getattr(django_auth_ldap.config, data)(member_attr='member') - else: - return getattr(django_auth_ldap.config, data)() + + params = self.get_depends_on() 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() + for attr in inspect.getargspec(cls.__init__).args[1:]: + if attr in params: + params_sanitized[attr] = params[attr] + + return cls(**params_sanitized) + + +class LDAPGroupTypeParamsField(fields.DictField, DependsOnMixin): + default_error_messages = { + 'invalid_keys': _('Invalid key(s): {invalid_keys}.'), + } + + def to_internal_value(self, value): + value = super(LDAPGroupTypeParamsField, self).to_internal_value(value) + if not value: + return value + group_type_str = self.get_depends_on() + group_type_str = group_type_str or '' + + group_type_cls = find_class_in_modules(group_type_str) + if not group_type_cls: + # Fail safe + return {} + + invalid_keys = set(value.keys()) - set(inspect.getargspec(group_type_cls.__init__).args[1:]) + if invalid_keys: + keys_display = json.dumps(list(invalid_keys)).lstrip('[').rstrip(']') + self.fail('invalid_keys', invalid_keys=keys_display) + return value 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..84144e0af3 --- /dev/null +++ b/awx/sso/ldap_group_types.py @@ -0,0 +1,75 @@ +# Copyright (c) 2018 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, name_attr='cn', ldap_group_user_attr='uid'): + self.ldap_group_user_attr = ldap_group_user_attr + super(PosixUIDGroupType, self).__init__(name_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 + diff --git a/awx/sso/tests/unit/test_fields.py b/awx/sso/tests/unit/test_fields.py index df09690c49..1113665241 100644 --- a/awx/sso/tests/unit/test_fields.py +++ b/awx/sso/tests/unit/test_fields.py @@ -1,11 +1,13 @@ import pytest +import mock from rest_framework.exceptions import ValidationError from awx.sso.fields import ( SAMLOrgAttrField, SAMLTeamAttrField, + LDAPGroupTypeParamsField, ) @@ -80,3 +82,22 @@ class TestSAMLTeamAttrField(): field.to_internal_value(data) assert str(e.value) == str(expected) + +class TestLDAPGroupTypeParamsField(): + + @pytest.mark.parametrize("group_type, data, expected", [ + ('LDAPGroupType', {'name_attr': 'user', 'bob': ['a', 'b'], 'scooter': 'hello'}, + ValidationError('Invalid key(s): "bob", "scooter".')), + ('MemberDNGroupType', {'name_attr': 'user', 'member_attr': 'west', 'bob': ['a', 'b'], 'scooter': 'hello'}, + ValidationError('Invalid key(s): "bob", "scooter".')), + ('PosixUIDGroupType', {'name_attr': 'user', 'member_attr': 'west', 'ldap_group_user_attr': 'legacyThing', + 'bob': ['a', 'b'], 'scooter': 'hello'}, + ValidationError('Invalid key(s): "bob", "member_attr", "scooter".')), + ]) + def test_internal_value_invalid(self, group_type, data, expected): + field = LDAPGroupTypeParamsField() + field.get_depends_on = mock.MagicMock(return_value=group_type) + + with pytest.raises(type(expected)) as e: + field.to_internal_value(data) + assert str(e.value) == str(expected) |