Skip to content

Commit

Permalink
Merge pull request ansible#1512 from chrismeyersfsu/feature-new_ldap_…
Browse files Browse the repository at this point in the history
…group_type

add ldap group type like posixGroupType
  • Loading branch information
matburt authored Mar 26, 2018
2 parents d6203b5 + b9b8502 commit 9637058
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 9 deletions.
7 changes: 5 additions & 2 deletions awx/conf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ def _preload_cache(self):
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:
Expand Down Expand Up @@ -368,7 +368,10 @@ def _get_local(self, name):
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.',
Expand Down
1 change: 1 addition & 0 deletions awx/sso/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={}):
Expand Down
20 changes: 20 additions & 0 deletions awx/sso/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
89 changes: 82 additions & 7 deletions awx/sso/fields.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# Python LDAP
import ldap
import awx

# Django
from django.utils.translation import ugettext_lazy as _
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
Expand All @@ -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
Expand Down Expand Up @@ -322,7 +360,7 @@ def to_internal_value(self, data):
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.'),
Expand All @@ -335,7 +373,7 @@ def __init__(self, choices=None, **kwargs):

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__
Expand All @@ -344,10 +382,47 @@ def to_internal_value(self, data):
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):
Expand Down
75 changes: 75 additions & 0 deletions awx/sso/ldap_group_types.py
Original file line number Diff line number Diff line change
@@ -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

21 changes: 21 additions & 0 deletions awx/sso/tests/unit/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@

import pytest
import mock

from rest_framework.exceptions import ValidationError

from awx.sso.fields import (
SAMLOrgAttrField,
SAMLTeamAttrField,
LDAPGroupTypeParamsField,
)


Expand Down Expand Up @@ -80,3 +82,22 @@ def test_internal_value_invalid(self, data, expected):
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)

0 comments on commit 9637058

Please sign in to comment.