diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 0fff829d23..c5bad0c6c4 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -238,11 +238,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour app_label = 'main' ordering = ('name',) - host_config_key = models.CharField( + host_config_key = prevent_search(models.CharField( max_length=1024, blank=True, default='', - ) + )) ask_diff_mode_on_launch = AskForField( blank=True, default=False, diff --git a/awx/main/tests/unit/utils/test_filters.py b/awx/main/tests/unit/utils/test_filters.py index f7d25d06f3..cd82e0f1f0 100644 --- a/awx/main/tests/unit/utils/test_filters.py +++ b/awx/main/tests/unit/utils/test_filters.py @@ -2,7 +2,6 @@ # Python import pytest import mock -from collections import namedtuple # AWX 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 -Field = namedtuple('Field', 'name') -Meta = namedtuple('Meta', 'fields') +class Field(object): + + 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: @@ -53,15 +70,32 @@ class mockObjects: 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: def __init__(self): print("Host mock created") 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()) 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", [ ('facts__facts__blank=""', Q(**{u"facts__facts__blank": u""})), ('"facts__facts__ space "="f"', Q(**{u"facts__facts__ space ": u"f"})), @@ -88,6 +122,16 @@ class TestSmartFilterQueryFromString(): SmartFilter.query_from_string(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", [ (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"}})), diff --git a/awx/main/utils/filters.py b/awx/main/utils/filters.py index 30daf338f2..5e6d3f4221 100644 --- a/awx/main/utils/filters.py +++ b/awx/main/utils/filters.py @@ -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()]) self.result = Host.objects.filter(q) 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 self.result = Host.objects.filter(**kwargs)