summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Jones <bsdmatburt@gmail.com>2018-03-26 21:29:01 +0200
committerGitHub <noreply@github.com>2018-03-26 21:29:01 +0200
commit96370584062a15271abafab7fc557ac2879aa38c (patch)
tree1eb5cf1500defaf5b837354188e107243f1c1267
parentMerge pull request #1679 from mabashian/workflow-node-start-bug (diff)
parentintrospect ldap group types for param validation (diff)
downloadawx-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.py7
-rw-r--r--awx/sso/backends.py1
-rw-r--r--awx/sso/conf.py20
-rw-r--r--awx/sso/fields.py89
-rw-r--r--awx/sso/ldap_group_types.py75
-rw-r--r--awx/sso/tests/unit/test_fields.py21
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)