mirror of
https://github.com/ansible/awx.git
synced 2026-03-26 13:25:02 -02:30
Merge pull request #2885 from ryanpetrello/fix-2874
apply sensitive field filtering to /api/v2/hosts/?host_filter
This commit is contained in:
@@ -238,11 +238,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
|
||||||
host_config_key = models.CharField(
|
host_config_key = prevent_search(models.CharField(
|
||||||
max_length=1024,
|
max_length=1024,
|
||||||
blank=True,
|
blank=True,
|
||||||
default='',
|
default='',
|
||||||
)
|
))
|
||||||
ask_diff_mode_on_launch = AskForField(
|
ask_diff_mode_on_launch = AskForField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
# Python
|
# Python
|
||||||
import pytest
|
import pytest
|
||||||
import mock
|
import mock
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.utils.filters import SmartFilter, ExternalLoggerEnabled
|
from awx.main.utils.filters import SmartFilter, ExternalLoggerEnabled
|
||||||
@@ -44,8 +43,26 @@ def test_log_configurable_severity(level, expect, dummy_log_record):
|
|||||||
assert filter.filter(dummy_log_record) is expect
|
assert filter.filter(dummy_log_record) is expect
|
||||||
|
|
||||||
|
|
||||||
Field = namedtuple('Field', 'name')
|
class Field(object):
|
||||||
Meta = namedtuple('Meta', 'fields')
|
|
||||||
|
def __init__(self, name, related_model=None, __prevent_search__=None):
|
||||||
|
self.name = name
|
||||||
|
self.related_model = related_model
|
||||||
|
self.__prevent_search__ = __prevent_search__
|
||||||
|
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
|
||||||
|
def __init__(self, fields):
|
||||||
|
self._fields = {
|
||||||
|
f.name: f for f in fields
|
||||||
|
}
|
||||||
|
self.object_name = 'Host'
|
||||||
|
self.fields_map = {}
|
||||||
|
self.fields = self._fields.values()
|
||||||
|
|
||||||
|
def get_field(self, f):
|
||||||
|
return self._fields.get(f)
|
||||||
|
|
||||||
|
|
||||||
class mockObjects:
|
class mockObjects:
|
||||||
@@ -53,15 +70,32 @@ class mockObjects:
|
|||||||
return Q(*args, **kwargs)
|
return Q(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class mockUser:
|
||||||
|
def __init__(self):
|
||||||
|
print("Host user created")
|
||||||
|
self._meta = Meta(fields=[
|
||||||
|
Field(name='password', __prevent_search__=True)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class mockHost:
|
class mockHost:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
print("Host mock created")
|
print("Host mock created")
|
||||||
self.objects = mockObjects()
|
self.objects = mockObjects()
|
||||||
self._meta = Meta(fields=(Field(name='name'), Field(name='description')))
|
fields = [
|
||||||
|
Field(name='name'),
|
||||||
|
Field(name='description'),
|
||||||
|
Field(name='created_by', related_model=mockUser())
|
||||||
|
]
|
||||||
|
self._meta = Meta(fields=fields)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('awx.main.utils.filters.get_model', return_value=mockHost())
|
@mock.patch('awx.main.utils.filters.get_model', return_value=mockHost())
|
||||||
class TestSmartFilterQueryFromString():
|
class TestSmartFilterQueryFromString():
|
||||||
|
@mock.patch(
|
||||||
|
'awx.api.filters.get_field_from_path',
|
||||||
|
lambda model, path: (model, path) # disable field filtering, because a__b isn't a real Host field
|
||||||
|
)
|
||||||
@pytest.mark.parametrize("filter_string,q_expected", [
|
@pytest.mark.parametrize("filter_string,q_expected", [
|
||||||
('facts__facts__blank=""', Q(**{u"facts__facts__blank": u""})),
|
('facts__facts__blank=""', Q(**{u"facts__facts__blank": u""})),
|
||||||
('"facts__facts__ space "="f"', Q(**{u"facts__facts__ space ": u"f"})),
|
('"facts__facts__ space "="f"', Q(**{u"facts__facts__ space ": u"f"})),
|
||||||
@@ -88,6 +122,16 @@ class TestSmartFilterQueryFromString():
|
|||||||
SmartFilter.query_from_string(filter_string)
|
SmartFilter.query_from_string(filter_string)
|
||||||
assert e.value.message == u"Invalid query " + filter_string
|
assert e.value.message == u"Invalid query " + filter_string
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("filter_string", [
|
||||||
|
'created_by__password__icontains=pbkdf2'
|
||||||
|
'search=foo or created_by__password__icontains=pbkdf2',
|
||||||
|
'created_by__password__icontains=pbkdf2 or search=foo',
|
||||||
|
])
|
||||||
|
def test_forbidden_filter_string(self, mock_get_host_model, filter_string):
|
||||||
|
with pytest.raises(Exception) as e:
|
||||||
|
SmartFilter.query_from_string(filter_string)
|
||||||
|
"Filtering on password is not allowed." in str(e)
|
||||||
|
|
||||||
@pytest.mark.parametrize("filter_string,q_expected", [
|
@pytest.mark.parametrize("filter_string,q_expected", [
|
||||||
(u'(a=abc\u1F5E3def)', Q(**{u"a": u"abc\u1F5E3def"})),
|
(u'(a=abc\u1F5E3def)', Q(**{u"a": u"abc\u1F5E3def"})),
|
||||||
(u'(ansible_facts__a=abc\u1F5E3def)', Q(**{u"ansible_facts__contains": {u"a": u"abc\u1F5E3def"}})),
|
(u'(ansible_facts__a=abc\u1F5E3def)', Q(**{u"ansible_facts__contains": {u"a": u"abc\u1F5E3def"}})),
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ class SmartFilter(object):
|
|||||||
q = reduce(lambda x, y: x | y, [models.Q(**{u'%s__icontains' % _k:_v}) for _k, _v in kwargs.items()])
|
q = reduce(lambda x, y: x | y, [models.Q(**{u'%s__icontains' % _k:_v}) for _k, _v in kwargs.items()])
|
||||||
self.result = Host.objects.filter(q)
|
self.result = Host.objects.filter(q)
|
||||||
else:
|
else:
|
||||||
|
# detect loops and restrict access to sensitive fields
|
||||||
|
# this import is intentional here to avoid a circular import
|
||||||
|
from awx.api.filters import FieldLookupBackend
|
||||||
|
FieldLookupBackend().get_field_from_lookup(Host, k)
|
||||||
kwargs[k] = v
|
kwargs[k] = v
|
||||||
self.result = Host.objects.filter(**kwargs)
|
self.result = Host.objects.filter(**kwargs)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user