mirror of
https://github.com/ansible/awx.git
synced 2024-10-31 15:21:13 +03:00
Merge pull request #721 from chrismeyersfsu/feature-2_factor
allow support for saml + 2-factor
This commit is contained in:
commit
1899795d08
@ -53,6 +53,47 @@ class StringListField(ListField):
|
||||
return super(StringListField, self).to_representation(value)
|
||||
|
||||
|
||||
class StringListBooleanField(ListField):
|
||||
|
||||
default_error_messages = {
|
||||
'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.'),
|
||||
}
|
||||
child = CharField()
|
||||
|
||||
def to_representation(self, value):
|
||||
try:
|
||||
if isinstance(value, (list, tuple)):
|
||||
return super(StringListBooleanField, self).to_representation(value)
|
||||
elif value in NullBooleanField.TRUE_VALUES:
|
||||
return True
|
||||
elif value in NullBooleanField.FALSE_VALUES:
|
||||
return False
|
||||
elif value in NullBooleanField.NULL_VALUES:
|
||||
return None
|
||||
elif isinstance(value, basestring):
|
||||
return self.child.to_representation(value)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
self.fail('type_error', input_type=type(value))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
if isinstance(data, (list, tuple)):
|
||||
return super(StringListBooleanField, self).to_internal_value(data)
|
||||
elif data in NullBooleanField.TRUE_VALUES:
|
||||
return True
|
||||
elif data in NullBooleanField.FALSE_VALUES:
|
||||
return False
|
||||
elif data in NullBooleanField.NULL_VALUES:
|
||||
return None
|
||||
elif isinstance(data, basestring):
|
||||
return self.child.run_validation(data)
|
||||
except TypeError:
|
||||
pass
|
||||
self.fail('type_error', input_type=type(data))
|
||||
|
||||
|
||||
class URLField(CharField):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@ -100,3 +141,25 @@ class KeyValueField(DictField):
|
||||
if not isinstance(value, six.string_types + six.integer_types + (float,)):
|
||||
self.fail('invalid_child', input=value)
|
||||
return ret
|
||||
|
||||
|
||||
class ListTuplesField(ListField):
|
||||
default_error_messages = {
|
||||
'type_error': _('Expected a list of tuples of max length 2 but got {input_type} instead.'),
|
||||
}
|
||||
|
||||
def to_representation(self, value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return super(ListTuplesField, self).to_representation(value)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(value))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, list):
|
||||
for x in data:
|
||||
if not isinstance(x, (list, tuple)) or len(x) > 2:
|
||||
self.fail('type_error', input_type=type(x))
|
||||
|
||||
return super(ListTuplesField, self).to_internal_value(data)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(data))
|
||||
|
86
awx/conf/tests/unit/test_fields.py
Normal file
86
awx/conf/tests/unit/test_fields.py
Normal file
@ -0,0 +1,86 @@
|
||||
import pytest
|
||||
|
||||
from rest_framework.fields import ValidationError
|
||||
from awx.conf.fields import StringListBooleanField, ListTuplesField
|
||||
|
||||
|
||||
class TestStringListBooleanField():
|
||||
|
||||
FIELD_VALUES = [
|
||||
("hello", "hello"),
|
||||
(("a", "b"), ["a", "b"]),
|
||||
(["a", "b", 1, 3.13, "foo", "bar", "foobar"], ["a", "b", "1", "3.13", "foo", "bar", "foobar"]),
|
||||
("True", True),
|
||||
("TRUE", True),
|
||||
("true", True),
|
||||
(True, True),
|
||||
("False", False),
|
||||
("FALSE", False),
|
||||
("false", False),
|
||||
(False, False),
|
||||
("", None),
|
||||
("null", None),
|
||||
("NULL", None),
|
||||
]
|
||||
|
||||
FIELD_VALUES_INVALID = [
|
||||
1.245,
|
||||
{"a": "b"},
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
||||
def test_to_internal_value_valid(self, value_in, value_known):
|
||||
field = StringListBooleanField()
|
||||
v = field.to_internal_value(value_in)
|
||||
assert v == value_known
|
||||
|
||||
@pytest.mark.parametrize("value", FIELD_VALUES_INVALID)
|
||||
def test_to_internal_value_invalid(self, value):
|
||||
field = StringListBooleanField()
|
||||
with pytest.raises(ValidationError) as e:
|
||||
field.to_internal_value(value)
|
||||
assert e.value.detail[0] == "Expected None, True, False, a string or list " \
|
||||
"of strings but got {} instead.".format(type(value))
|
||||
|
||||
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
||||
def test_to_representation_valid(self, value_in, value_known):
|
||||
field = StringListBooleanField()
|
||||
v = field.to_representation(value_in)
|
||||
assert v == value_known
|
||||
|
||||
@pytest.mark.parametrize("value", FIELD_VALUES_INVALID)
|
||||
def test_to_representation_invalid(self, value):
|
||||
field = StringListBooleanField()
|
||||
with pytest.raises(ValidationError) as e:
|
||||
field.to_representation(value)
|
||||
assert e.value.detail[0] == "Expected None, True, False, a string or list " \
|
||||
"of strings but got {} instead.".format(type(value))
|
||||
|
||||
|
||||
class TestListTuplesField():
|
||||
|
||||
FIELD_VALUES = [
|
||||
([('a', 'b'), ('abc', '123')], [("a", "b"), ("abc", "123")]),
|
||||
]
|
||||
|
||||
FIELD_VALUES_INVALID = [
|
||||
("abc", type("abc")),
|
||||
([('a', 'b', 'c'), ('abc', '123', '456')], type(('a',))),
|
||||
(['a', 'b'], type('a')),
|
||||
(123, type(123)),
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("value_in, value_known", FIELD_VALUES)
|
||||
def test_to_internal_value_valid(self, value_in, value_known):
|
||||
field = ListTuplesField()
|
||||
v = field.to_internal_value(value_in)
|
||||
assert v == value_known
|
||||
|
||||
@pytest.mark.parametrize("value, t", FIELD_VALUES_INVALID)
|
||||
def test_to_internal_value_invalid(self, value, t):
|
||||
field = ListTuplesField()
|
||||
with pytest.raises(ValidationError) as e:
|
||||
field.to_internal_value(value)
|
||||
assert e.value.detail[0] == "Expected a list of tuples of max length 2 " \
|
||||
"but got {} instead.".format(t)
|
||||
|
@ -1061,6 +1061,71 @@ register(
|
||||
feature_required='enterprise_auth',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_SECURITY_CONFIG',
|
||||
field_class=fields.SAMLSecurityField,
|
||||
allow_null=True,
|
||||
default={'requestedAuthnContext': False},
|
||||
label=_('SAML Security Config'),
|
||||
help_text=_('A dict of key value pairs that are passed to the underlying'
|
||||
' python-saml security setting'
|
||||
' https://github.com/onelogin/python-saml#settings'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=collections.OrderedDict([
|
||||
("nameIdEncrypted", False),
|
||||
("authnRequestsSigned", False),
|
||||
("logoutRequestSigned", False),
|
||||
("logoutResponseSigned", False),
|
||||
("signMetadata", False),
|
||||
("wantMessagesSigned", False),
|
||||
("wantAssertionsSigned", False),
|
||||
("wantAssertionsEncrypted", False),
|
||||
("wantNameId", True),
|
||||
("wantNameIdEncrypted", False),
|
||||
("wantAttributeStatement", True),
|
||||
("requestedAuthnContext", True),
|
||||
("requestedAuthnContextComparison", "exact"),
|
||||
("metadataValidUntil", "2015-06-26T20:00:00Z"),
|
||||
("metadataCacheDuration", "PT518400S"),
|
||||
("signatureAlgorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
|
||||
("digestAlgorithm", "http://www.w3.org/2000/09/xmldsig#sha1"),
|
||||
]),
|
||||
feature_required='enterprise_auth',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_SP_EXTRA',
|
||||
field_class=fields.DictField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('SAML Service Provider extra configuration data'),
|
||||
help_text=_('A dict of key value pairs to be passed to the underlying'
|
||||
' python-saml Service Provider configuration setting.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=collections.OrderedDict(),
|
||||
feature_required='enterprise_auth',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_EXTRA_DATA',
|
||||
field_class=fields.ListTuplesField,
|
||||
allow_null=True,
|
||||
default=None,
|
||||
label=_('SAML IDP to extra_data attribute mapping'),
|
||||
help_text=_('A list of tuples that maps IDP attributes to extra_attributes.'
|
||||
' Each attribute will be a list of values, even if only 1 value.'),
|
||||
category=_('SAML'),
|
||||
category_slug='saml',
|
||||
placeholder=[
|
||||
('attribute_name', 'extra_data_name_for_attribute'),
|
||||
('department', 'department'),
|
||||
('manager_full_name', 'manager_full_name')
|
||||
],
|
||||
feature_required='enterprise_auth',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_SAML_ORGANIZATION_MAP',
|
||||
field_class=fields.SocialOrganizationMapField,
|
||||
|
@ -345,41 +345,10 @@ class LDAPUserFlagsField(fields.DictField):
|
||||
return data
|
||||
|
||||
|
||||
class LDAPDNMapField(fields.ListField):
|
||||
class LDAPDNMapField(fields.StringListBooleanField):
|
||||
|
||||
default_error_messages = {
|
||||
'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.'),
|
||||
}
|
||||
child = LDAPDNField()
|
||||
|
||||
def to_representation(self, value):
|
||||
if isinstance(value, (list, tuple)):
|
||||
return super(LDAPDNMapField, self).to_representation(value)
|
||||
elif value in fields.NullBooleanField.TRUE_VALUES:
|
||||
return True
|
||||
elif value in fields.NullBooleanField.FALSE_VALUES:
|
||||
return False
|
||||
elif value in fields.NullBooleanField.NULL_VALUES:
|
||||
return None
|
||||
elif isinstance(value, basestring):
|
||||
return self.child.to_representation(value)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(value))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, (list, tuple)):
|
||||
return super(LDAPDNMapField, self).to_internal_value(data)
|
||||
elif data in fields.NullBooleanField.TRUE_VALUES:
|
||||
return True
|
||||
elif data in fields.NullBooleanField.FALSE_VALUES:
|
||||
return False
|
||||
elif data in fields.NullBooleanField.NULL_VALUES:
|
||||
return None
|
||||
elif isinstance(data, basestring):
|
||||
return self.child.run_validation(data)
|
||||
else:
|
||||
self.fail('type_error', input_type=type(data))
|
||||
|
||||
|
||||
class BaseDictWithChildField(fields.DictField):
|
||||
|
||||
@ -649,3 +618,28 @@ class SAMLIdPField(BaseDictWithChildField):
|
||||
class SAMLEnabledIdPsField(fields.DictField):
|
||||
|
||||
child = SAMLIdPField()
|
||||
|
||||
|
||||
class SAMLSecurityField(fields.DictField):
|
||||
|
||||
child_fields = {
|
||||
'nameIdEncrypted': fields.BooleanField(required=False),
|
||||
'authnRequestsSigned': fields.BooleanField(required=False),
|
||||
'logoutRequestSigned': fields.BooleanField(required=False),
|
||||
'logoutResponseSigned': fields.BooleanField(required=False),
|
||||
'signMetadata': fields.BooleanField(required=False),
|
||||
'wantMessagesSigned': fields.BooleanField(required=False),
|
||||
'wantAssertionsSigned': fields.BooleanField(required=False),
|
||||
'wantAssertionsEncrypted': fields.BooleanField(required=False),
|
||||
'wantNameId': fields.BooleanField(required=False),
|
||||
'wantNameIdEncrypted': fields.BooleanField(required=False),
|
||||
'wantAttributeStatement': fields.BooleanField(required=False),
|
||||
'requestedAuthnContext': fields.StringListBooleanField(required=False),
|
||||
'requestedAuthnContextComparison': fields.CharField(required=False),
|
||||
'metadataValidUntil': fields.CharField(allow_null=True, required=False),
|
||||
'metadataCacheDuration': fields.CharField(allow_null=True, required=False),
|
||||
'signatureAlgorithm': fields.CharField(allow_null=True, required=False),
|
||||
'digestAlgorithm': fields.CharField(allow_null=True, required=False),
|
||||
}
|
||||
allow_unknown_keys = True
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user