mirror of
https://github.com/ansible/awx.git
synced 2026-01-21 22:48:02 -03:30
Merge pull request #1512 from chrismeyersfsu/feature-new_ldap_group_type
add ldap group type like posixGroupType
This commit is contained in:
commit
9637058406
@ -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.',
|
||||
|
||||
@ -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={}):
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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):
|
||||
|
||||
75
awx/sso/ldap_group_types.py
Normal file
75
awx/sso/ldap_group_types.py
Normal 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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user