mirror of
https://github.com/ansible/awx.git
synced 2026-02-19 12:10:06 -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:
@@ -47,7 +47,7 @@ from awx.main.constants import (
|
|||||||
)
|
)
|
||||||
from awx.main.models import * # noqa
|
from awx.main.models import * # noqa
|
||||||
from awx.main.models.base import NEW_JOB_TYPE_CHOICES
|
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 (
|
from awx.main.utils import (
|
||||||
get_type_for_model, get_model_for_type, timestamp_apiformat,
|
get_type_for_model, get_model_for_type, timestamp_apiformat,
|
||||||
camelcase_to_underscore, getattrd, parse_yaml_or_json,
|
camelcase_to_underscore, getattrd, parse_yaml_or_json,
|
||||||
@@ -1542,6 +1542,18 @@ class InventorySerializer(BaseSerializerWithVariables):
|
|||||||
def validate_host_filter(self, host_filter):
|
def validate_host_filter(self, host_filter):
|
||||||
if host_filter:
|
if host_filter:
|
||||||
try:
|
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)
|
SmartFilter().query_from_string(host_filter)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise models.base.ValidationError(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=レッドハット'
|
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", [
|
@pytest.mark.parametrize("role_field,expected_status_code", [
|
||||||
(None, 403),
|
(None, 403),
|
||||||
('admin_role', 201),
|
('admin_role', 201),
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class TestSmartFilterQueryFromString():
|
|||||||
('a__b__c=false', Q(**{u"a__b__c": False})),
|
('a__b__c=false', Q(**{u"a__b__c": False})),
|
||||||
('a__b__c=null', Q(**{u"a__b__c": None})),
|
('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="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"})),
|
||||||
#('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,
|
CharsNotIn,
|
||||||
ParseException,
|
ParseException,
|
||||||
)
|
)
|
||||||
|
import logging
|
||||||
from logging import Filter, _nameToLevel
|
from logging import Filter, _nameToLevel
|
||||||
|
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -19,6 +19,8 @@ from awx.main.utils.common import get_search_fields
|
|||||||
|
|
||||||
__all__ = ['SmartFilter', 'ExternalLoggerEnabled']
|
__all__ = ['SmartFilter', 'ExternalLoggerEnabled']
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.main.utils')
|
||||||
|
|
||||||
|
|
||||||
class FieldFromSettings(object):
|
class FieldFromSettings(object):
|
||||||
"""
|
"""
|
||||||
@@ -171,10 +173,25 @@ class SmartFilter(object):
|
|||||||
relationship refered to to see if it's a jsonb type.
|
relationship refered to to see if it's a jsonb type.
|
||||||
'''
|
'''
|
||||||
def _json_path_to_contains(self, k, v):
|
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):
|
if not k.startswith(SmartFilter.SEARCHABLE_RELATIONSHIP):
|
||||||
v = self.strip_quotes_traditional_logic(v)
|
v = self.strip_quotes_traditional_logic(v)
|
||||||
return (k, 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
|
# Strip off leading relationship key
|
||||||
if k.startswith(SmartFilter.SEARCHABLE_RELATIONSHIP + '__'):
|
if k.startswith(SmartFilter.SEARCHABLE_RELATIONSHIP + '__'):
|
||||||
strip_len = len(SmartFilter.SEARCHABLE_RELATIONSHIP) + 2
|
strip_len = len(SmartFilter.SEARCHABLE_RELATIONSHIP) + 2
|
||||||
|
|||||||
Reference in New Issue
Block a user