prevent field lookups on Host.ansible_facts keys (it doesn't work)

under the hood, Host.ansible_facts is a postgres jsonb field which
performs match operations using the JSON containment operator (@>)

this operator _only_ works on exact matches on containment (i.e.,
"does the `ansible_distribution` jsonb value contain _this exact_ JSON
structure"):

SELECT ...
FROM main_host
WHERE ansible_facts @> '{"ansible_distribution": "centos"}'

SELECT ...
FROM main_host
WHERE ansible_facts @> '{"packages": {"dnsmasq": [{"version": 2}]}}'

postgres does _not_ expose any operator for fuzzy or lookup-based
matches with this operator, so host filter values like these don't
really make sense (postgres can't _filter_ in the way intended in these
examples):

ansible_distribution__startswith=\"Cent\"
ansible_distribution__icontains=\"CentOS\"
ansible_facts__packages__dnsmasq[]__version__startswith=\"2\"
This commit is contained in:
Ryan Petrello 2019-02-04 21:28:50 -05:00
parent cab6b8b333
commit bb5312f4fc
No known key found for this signature in database
GPG Key ID: F2AA5F2122351777
4 changed files with 62 additions and 2 deletions

View File

@ -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)

View File

@ -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),

View File

@ -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"})),
])

View File

@ -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