mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 18:40:01 -03:30
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:
parent
cab6b8b333
commit
bb5312f4fc
@ -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)
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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"})),
|
||||
])
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user