Merge pull request #1512 from chrismeyersfsu/feature-new_ldap_group_type

add ldap group type like posixGroupType
This commit is contained in:
Matthew Jones 2018-03-26 12:29:01 -07:00 committed by GitHub
commit 9637058406
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 9 deletions

View File

@ -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.',

View File

@ -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={}):

View File

@ -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(

View File

@ -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):

View File

@ -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

View File

@ -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)