diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bb74b4c764..51cd7c8bbb 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -47,7 +47,7 @@ from awx.main.constants import ( ) from awx.main.models import * # noqa from awx.main.models.base import NEW_JOB_TYPE_CHOICES -from awx.main.fields import ImplicitRoleField +from awx.main.fields import ImplicitRoleField, JSONBField from awx.main.utils import ( get_type_for_model, get_model_for_type, timestamp_apiformat, camelcase_to_underscore, getattrd, parse_yaml_or_json, @@ -1542,6 +1542,18 @@ class InventorySerializer(BaseSerializerWithVariables): def validate_host_filter(self, host_filter): if host_filter: try: + for match in JSONBField.get_lookups().keys(): + if match == 'exact': + # __exact is allowed + continue + match = '__{}'.format(match) + if re.match( + 'ansible_facts[^=]+{}='.format(match), + host_filter + ): + raise models.base.ValidationError({ + 'host_filter': 'ansible_facts does not support searching with {}'.format(match) + }) SmartFilter().query_from_string(host_filter) except RuntimeError as e: raise models.base.ValidationError(e) diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index a9cecf8ba0..23ed3a4f7d 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -281,6 +281,36 @@ def test_host_filter_unicode(post, admin_user, organization): assert si.host_filter == u'ansible_facts__ansible_distribution=レッドハット' +@pytest.mark.django_db +@pytest.mark.parametrize("lookup", ['icontains', 'has_keys']) +def test_host_filter_invalid_ansible_facts_lookup(post, admin_user, organization, lookup): + resp = post( + reverse('api:inventory_list'), + data={ + 'name': 'smart inventory', 'kind': 'smart', + 'organization': organization.pk, + 'host_filter': u'ansible_facts__ansible_distribution__{}=cent'.format(lookup) + }, + user=admin_user, + expect=400 + ) + assert 'ansible_facts does not support searching with __{}'.format(lookup) in json.dumps(resp.data) + + +@pytest.mark.django_db +def test_host_filter_ansible_facts_exact(post, admin_user, organization): + post( + reverse('api:inventory_list'), + data={ + 'name': 'smart inventory', 'kind': 'smart', + 'organization': organization.pk, + 'host_filter': 'ansible_facts__ansible_distribution__exact="CentOS"' + }, + user=admin_user, + expect=201 + ) + + @pytest.mark.parametrize("role_field,expected_status_code", [ (None, 403), ('admin_role', 201), diff --git a/awx/main/tests/unit/utils/test_filters.py b/awx/main/tests/unit/utils/test_filters.py index ec59eee9e4..22444ab1b9 100644 --- a/awx/main/tests/unit/utils/test_filters.py +++ b/awx/main/tests/unit/utils/test_filters.py @@ -105,6 +105,7 @@ class TestSmartFilterQueryFromString(): ('a__b__c=false', Q(**{u"a__b__c": False})), ('a__b__c=null', Q(**{u"a__b__c": None})), ('ansible_facts__a="true"', Q(**{u"ansible_facts__contains": {u"a": u"true"}})), + ('ansible_facts__a__exact="true"', Q(**{u"ansible_facts__contains": {u"a": u"true"}})), #('"a__b\"__c"="true"', Q(**{u"a__b\"__c": "true"})), #('a__b\"__c="true"', Q(**{u"a__b\"__c": "true"})), ]) diff --git a/awx/main/utils/filters.py b/awx/main/utils/filters.py index faa1eca8fe..80f197390b 100644 --- a/awx/main/utils/filters.py +++ b/awx/main/utils/filters.py @@ -8,9 +8,9 @@ from pyparsing import ( CharsNotIn, ParseException, ) +import logging from logging import Filter, _nameToLevel - from django.apps import apps from django.db import models from django.conf import settings @@ -19,6 +19,8 @@ from awx.main.utils.common import get_search_fields __all__ = ['SmartFilter', 'ExternalLoggerEnabled'] +logger = logging.getLogger('awx.main.utils') + class FieldFromSettings(object): """ @@ -171,10 +173,25 @@ class SmartFilter(object): relationship refered to to see if it's a jsonb type. ''' def _json_path_to_contains(self, k, v): + from awx.main.fields import JSONBField # avoid a circular import if not k.startswith(SmartFilter.SEARCHABLE_RELATIONSHIP): v = self.strip_quotes_traditional_logic(v) return (k, v) + for match in JSONBField.get_lookups().keys(): + match = '__{}'.format(match) + if k.endswith(match): + if match == '__exact': + # appending __exact is basically a no-op, because that's + # what the query means if you leave it off + k = k[:-len(match)] + logger.error( + 'host_filter:{} does not support searching with {}'.format( + SmartFilter.SEARCHABLE_RELATIONSHIP, + match + ) + ) + # Strip off leading relationship key if k.startswith(SmartFilter.SEARCHABLE_RELATIONSHIP + '__'): strip_len = len(SmartFilter.SEARCHABLE_RELATIONSHIP) + 2