mirror of
https://github.com/ansible/awx.git
synced 2026-10-29 22:03:20 -02:30
[AAP-78694] Add CleanTextMixin to settings serializer (#16661)
* Add cleantextmixin to settings serializer * Make excluded_fields a cached property * Add test coverage for category_slug all * Fix OPTIONS 500 for settings CleanTextMixin pattern injection * Apply validation for json and list fields --------- Co-authored-by: Peter Braun <pbraun@redhat.com>
This commit is contained in:
co-authored by
Peter Braun
parent
2a6dd41186
commit
4c290176ef
+25
-5
@@ -218,9 +218,14 @@ def reverse_gfk(content_object, request):
|
||||
class PlainSerializerCleanTextMixin(CleanTextMixin):
|
||||
"""CleanTextMixin for plain `serializers.Serializer` subclasses with no
|
||||
backing Django model (AAP-78694). Overrides ONLY field discovery -- treats
|
||||
the serializer's own declared CharFields as the text fields to validate.
|
||||
Everything else (Tier 1/2 dispatch, exclusions, audit logging, the
|
||||
enforcement gate) is reused unchanged from CleanTextMixin.
|
||||
the serializer's own declared fields as the text/JSON containers to validate.
|
||||
Everything else (Tier 1/2 dispatch, nested JSON walk, exclusions, audit
|
||||
logging, the enforcement gate) is reused unchanged from CleanTextMixin.
|
||||
|
||||
CharFields (including URLField) are Tier 1/2 top-level text. ListField,
|
||||
DictField, and JSONField values are walked with CleanTextMixin's nested
|
||||
string validation (same path as model JSONFields) so settings like
|
||||
StringListField / KeyValueField are covered.
|
||||
|
||||
Known limitation: unlike the model-field path, this can't replicate the
|
||||
get_internal_type() exclusion of SlugField/IPAddressField-style fields
|
||||
@@ -228,9 +233,24 @@ class PlainSerializerCleanTextMixin(CleanTextMixin):
|
||||
using this mixin today, since none declare such fields.
|
||||
"""
|
||||
|
||||
# Structured containers whose string leaves should get validate_free_text.
|
||||
# Matches model CleanTextMixin's JSONField walk for settings registry
|
||||
# ListField/DictField/KeyValueField (and DRF JSONField if used).
|
||||
_NESTED_TEXT_CONTAINER_FIELDS = (
|
||||
serializers.ListField,
|
||||
serializers.DictField,
|
||||
serializers.JSONField,
|
||||
)
|
||||
|
||||
def _classify_fields(self, model):
|
||||
text_fields = [name for name, field in self.fields.items() if isinstance(field, serializers.CharField)]
|
||||
return text_fields, []
|
||||
text_fields = []
|
||||
json_fields = []
|
||||
for name, field in self.fields.items():
|
||||
if isinstance(field, serializers.CharField):
|
||||
text_fields.append(name)
|
||||
elif isinstance(field, self._NESTED_TEXT_CONTAINER_FIELDS):
|
||||
json_fields.append(name)
|
||||
return text_fields, json_fields
|
||||
|
||||
|
||||
class _CopySerializerFakeModel:
|
||||
|
||||
@@ -18,6 +18,9 @@ lands on DAB devel, injection is a no-op so Controller can still import and run.
|
||||
|
||||
import copy
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ansible_base.lib.serializers.mixins import CleanTextMixin
|
||||
from ansible_base.lib.utils.settings import get_setting
|
||||
|
||||
try:
|
||||
@@ -30,6 +33,13 @@ try:
|
||||
except ImportError: # pragma: no cover - DAB without AAP-85987
|
||||
_dab_inject_clean_text_patterns = None
|
||||
|
||||
try:
|
||||
from ansible_base.lib.metadata import get_tier1_pattern as _get_tier1_pattern
|
||||
from ansible_base.lib.metadata import get_tier2_pattern as _get_tier2_pattern
|
||||
except ImportError: # pragma: no cover - DAB without AAP-85987
|
||||
_get_tier1_pattern = None
|
||||
_get_tier2_pattern = None
|
||||
|
||||
try:
|
||||
from ansible_base.lib.metadata import TIER2_PATTERN_DESCRIPTION
|
||||
except ImportError: # pragma: no cover - DAB without AAP-85987
|
||||
@@ -209,13 +219,62 @@ def collect_survey_spec_text_errors(new_spec, old_spec=None):
|
||||
return errors or None
|
||||
|
||||
|
||||
def _uses_fake_model_meta(serializer):
|
||||
"""True when Meta.model is a stand-in without Django's Options.get_field.
|
||||
|
||||
Plain serializers (SettingSingletonSerializer, CopySerializer) use a fake
|
||||
Meta.model so CleanTextMixin audit logs have app_label/object_name. DAB's
|
||||
inject_clean_text_patterns calls model._meta.get_field() and would 500 on
|
||||
OPTIONS for those endpoints -- handle them here instead.
|
||||
"""
|
||||
model = getattr(getattr(serializer, 'Meta', None), 'model', None)
|
||||
if model is None:
|
||||
return False
|
||||
meta = getattr(model, '_meta', None)
|
||||
return meta is not None and not hasattr(meta, 'get_field')
|
||||
|
||||
|
||||
def _inject_declared_char_field_patterns(field, field_info, serializer):
|
||||
"""Tier 1/2 OPTIONS hints for declared CharFields on plain CleanText serializers."""
|
||||
if _get_tier1_pattern is None or _get_tier2_pattern is None:
|
||||
return field_info
|
||||
if not get_setting('ENHANCED_INPUT_VALIDATION_ENABLED', False):
|
||||
return field_info
|
||||
if not isinstance(serializer, CleanTextMixin):
|
||||
return field_info
|
||||
if not isinstance(field, serializers.CharField):
|
||||
return field_info
|
||||
|
||||
field_name = field.field_name
|
||||
if field_name in getattr(serializer, 'excluded_fields', frozenset()):
|
||||
return field_info
|
||||
|
||||
if field_name in getattr(serializer, 'name_fields', frozenset()):
|
||||
tier1 = _get_tier1_pattern()
|
||||
field_info['pattern'] = tier1['pattern']
|
||||
field_info['patternDescription'] = tier1['description']
|
||||
field_info['flags'] = tier1['flags']
|
||||
field_info['normalize'] = tier1['normalize']
|
||||
else:
|
||||
tier2 = _get_tier2_pattern()
|
||||
field_info['pattern'] = tier2['pattern']
|
||||
field_info['patternDescription'] = tier2['description']
|
||||
field_info['flags'] = tier2['flags']
|
||||
|
||||
return field_info
|
||||
|
||||
|
||||
def inject_top_level_clean_text_patterns(field, field_info):
|
||||
"""Advertise DAB CleanTextMixin Tier 1/Tier 2 patterns on a top-level serializer field.
|
||||
|
||||
Delegates entirely to DAB's ``inject_clean_text_patterns``, which no-ops unless
|
||||
``ENHANCED_INPUT_VALIDATION_ENABLED`` is on and the field's serializer mixes in
|
||||
``CleanTextMixin``. No-op when the DAB helper is missing.
|
||||
For model-backed CleanTextMixin serializers, delegates to DAB's
|
||||
``inject_clean_text_patterns``. For plain serializers with a fake Meta.model
|
||||
(no ``_meta.get_field``), injects the same Tier 1/2 hints via DAB's public
|
||||
``get_tier1_pattern`` / ``get_tier2_pattern`` helpers so OPTIONS still works.
|
||||
"""
|
||||
serializer = getattr(field, 'parent', None)
|
||||
if serializer is not None and _uses_fake_model_meta(serializer):
|
||||
return _inject_declared_char_field_patterns(field, field_info, serializer)
|
||||
if _dab_inject_clean_text_patterns is None:
|
||||
return field_info
|
||||
return _dab_inject_clean_text_patterns(field, field_info)
|
||||
|
||||
+62
-6
@@ -1,9 +1,12 @@
|
||||
# Django
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework import serializers
|
||||
|
||||
# AWX
|
||||
from awx.api.fields import VerbatimField
|
||||
from awx.api.serializers import BaseSerializer
|
||||
from awx.api.serializers import BaseSerializer, PlainSerializerCleanTextMixin
|
||||
from awx.conf.models import Setting
|
||||
from awx.conf import settings_registry
|
||||
|
||||
@@ -50,8 +53,58 @@ class SettingFieldMixin(object):
|
||||
return super(SettingFieldMixin, self).to_representation(obj)
|
||||
|
||||
|
||||
class SettingSingletonSerializer(serializers.Serializer):
|
||||
"""Present a group of settings (by category) as a single object."""
|
||||
class _SettingSingletonFakeOpts:
|
||||
"""Minimal stand-in for Django model._meta.
|
||||
|
||||
CleanTextMixin audit logs need app_label/object_name. OPTIONS metadata
|
||||
(awx.api.metadata.get_field_info) also walks Meta.model._meta.fields when
|
||||
Meta.model is present -- an empty tuple keeps that path from crashing.
|
||||
"""
|
||||
|
||||
app_label = 'conf'
|
||||
object_name = 'SettingSingleton'
|
||||
verbose_name = 'setting singleton'
|
||||
fields = ()
|
||||
|
||||
@property
|
||||
def concrete_model(self):
|
||||
return _SettingSingletonFakeModel
|
||||
|
||||
|
||||
class _SettingSingletonFakeModel:
|
||||
"""Stand-in for Meta.model on SettingSingletonSerializer (plain Serializer).
|
||||
|
||||
Never introspected for real fields -- PlainSerializerCleanTextMixin discovers
|
||||
CharField / ListField / DictField / JSONField entries from self.fields instead.
|
||||
"""
|
||||
|
||||
_meta = _SettingSingletonFakeOpts()
|
||||
|
||||
|
||||
class SettingSingletonSerializer(PlainSerializerCleanTextMixin, serializers.Serializer):
|
||||
"""Present a group of settings (by category) as a single object.
|
||||
|
||||
Uses PlainSerializerCleanTextMixin because there is no Django model whose
|
||||
fields match the dynamic settings registry. Top-level CharFields get Tier
|
||||
1/2 text validation; ListField/DictField/JSONField string children are
|
||||
walked via CleanTextMixin's nested JSON validation path.
|
||||
"""
|
||||
|
||||
# CUSTOM_LOGIN_INFO deliberately allows HTML fragments (ui/conf.py).
|
||||
# CUSTOM_LOGO is a data:image/...;base64,... URI; Tier 2 blocks data: schemes.
|
||||
# AWX_TASK_ENV / GALAXY_TASK_ENV are key/value maps whose values often use
|
||||
# shell/env expansion (${VAR}, $(...)) or similar — Tier 2's injection
|
||||
# blocklist would reject legitimate automation env config.
|
||||
# Encrypted settings (passwords/tokens/PEMs) are excluded dynamically below.
|
||||
_ALWAYS_EXCLUDED = frozenset({'CUSTOM_LOGIN_INFO', 'CUSTOM_LOGO', 'AWX_TASK_ENV', 'GALAXY_TASK_ENV'})
|
||||
|
||||
class Meta:
|
||||
model = _SettingSingletonFakeModel
|
||||
|
||||
@cached_property
|
||||
def excluded_fields(self):
|
||||
encrypted = frozenset(s for s in settings_registry.get_registered_settings() if settings_registry.is_setting_encrypted(s))
|
||||
return self._ALWAYS_EXCLUDED | encrypted
|
||||
|
||||
def __init__(self, instance=None, data=serializers.empty, **kwargs):
|
||||
# Instance (if given) should be an object with attributes for all of the
|
||||
@@ -67,9 +120,12 @@ class SettingSingletonSerializer(serializers.Serializer):
|
||||
if self.context['view'].kwargs.get('category_slug', '') == 'all':
|
||||
for validate_func in settings_registry._validate_registry.values():
|
||||
attrs = validate_func(self, attrs)
|
||||
return attrs
|
||||
custom_validate = settings_registry.get_registered_validate_func(category_slug)
|
||||
return custom_validate(self, attrs) if custom_validate else attrs
|
||||
else:
|
||||
custom_validate = settings_registry.get_registered_validate_func(category_slug)
|
||||
if custom_validate:
|
||||
attrs = custom_validate(self, attrs)
|
||||
# Chain into CleanTextMixin (via PlainSerializerCleanTextMixin).
|
||||
return super().validate(attrs)
|
||||
|
||||
def get_fields(self):
|
||||
fields = super(SettingSingletonSerializer, self).get_fields()
|
||||
|
||||
@@ -162,3 +162,148 @@ def test_setting_singleton_delete_no_read_only_fields(api_request, dummy_setting
|
||||
api_request('delete', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||
response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}))
|
||||
assert response.data['FOO_BAR'] == 23
|
||||
|
||||
|
||||
#
|
||||
# CleanTextMixin on SettingSingletonSerializer (via PlainSerializerCleanTextMixin)
|
||||
#
|
||||
|
||||
UNSAFE_SETTING_INPUT = '<script>x</script>'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def enforce_clean_text():
|
||||
with mock.patch('ansible_base.lib.serializers.mixins.get_setting', return_value=True):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_rejects_unsafe_char_field(api_request, dummy_setting, enforce_clean_text):
|
||||
with (
|
||||
dummy_setting('FOO_BAR', field_class=fields.CharField, allow_blank=True, default='', category='FooBar', category_slug='foobar'),
|
||||
mock.patch('awx.conf.views.clear_setting_cache'),
|
||||
):
|
||||
response = api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_BAR': UNSAFE_SETTING_INPUT})
|
||||
assert response.status_code == 400
|
||||
assert 'FOO_BAR' in response.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_rejects_unsafe_char_field_all_category(api_request, dummy_setting, dummy_validate, enforce_clean_text):
|
||||
# category_slug='all' runs every registered validate_func, then CleanTextMixin.
|
||||
# A validator on a different category only participates via that loop.
|
||||
validate_calls = []
|
||||
|
||||
def record_validate(serializer, attrs):
|
||||
validate_calls.append(True)
|
||||
return attrs
|
||||
|
||||
with (
|
||||
dummy_setting('FOO_BAR', field_class=fields.CharField, allow_blank=True, default='', category='FooBar', category_slug='foobar'),
|
||||
dummy_validate('other', record_validate),
|
||||
mock.patch('awx.conf.views.clear_setting_cache'),
|
||||
):
|
||||
response = api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'all'}), data={'FOO_BAR': UNSAFE_SETTING_INPUT})
|
||||
assert validate_calls
|
||||
assert response.status_code == 400
|
||||
assert 'FOO_BAR' in response.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_skips_encrypted_fields(api_request, dummy_setting, enforce_clean_text):
|
||||
with (
|
||||
dummy_setting('FOO_SECRET', field_class=fields.CharField, encrypted=True, allow_blank=True, default='', category='FooBar', category_slug='foobar'),
|
||||
mock.patch('awx.conf.views.clear_setting_cache'),
|
||||
):
|
||||
response = api_request('patch', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), data={'FOO_SECRET': UNSAFE_SETTING_INPUT})
|
||||
assert response.status_code == 200
|
||||
assert decrypt_field(Setting.objects.get(key='FOO_SECRET'), 'value') == UNSAFE_SETTING_INPUT
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_skips_custom_login_info_html(api_request, enforce_clean_text):
|
||||
with mock.patch('awx.conf.views.clear_setting_cache'):
|
||||
response = api_request(
|
||||
'patch',
|
||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ui'}),
|
||||
data={'CUSTOM_LOGIN_INFO': '<p>Legal notice</p>'},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data['CUSTOM_LOGIN_INFO'] == '<p>Legal notice</p>'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_rejects_unsafe_string_list_item(api_request, dummy_setting, enforce_clean_text):
|
||||
with (
|
||||
dummy_setting(
|
||||
'FOO_LIST',
|
||||
field_class=fields.StringListField,
|
||||
default=[],
|
||||
category='FooBar',
|
||||
category_slug='foobar',
|
||||
),
|
||||
mock.patch('awx.conf.views.clear_setting_cache'),
|
||||
):
|
||||
response = api_request(
|
||||
'patch',
|
||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
||||
data={'FOO_LIST': ['ok-value', UNSAFE_SETTING_INPUT]},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert 'FOO_LIST' in response.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_rejects_unsafe_dict_value(api_request, dummy_setting, enforce_clean_text):
|
||||
with (
|
||||
dummy_setting(
|
||||
'FOO_MAP',
|
||||
field_class=fields.KeyValueField,
|
||||
default={},
|
||||
category='FooBar',
|
||||
category_slug='foobar',
|
||||
),
|
||||
mock.patch('awx.conf.views.clear_setting_cache'),
|
||||
):
|
||||
response = api_request(
|
||||
'patch',
|
||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
||||
data={'FOO_MAP': {'HTTP_PROXY': UNSAFE_SETTING_INPUT}},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert 'FOO_MAP' in response.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_allows_safe_string_list(api_request, dummy_setting, enforce_clean_text):
|
||||
with (
|
||||
dummy_setting(
|
||||
'FOO_LIST',
|
||||
field_class=fields.StringListField,
|
||||
default=[],
|
||||
category='FooBar',
|
||||
category_slug='foobar',
|
||||
),
|
||||
mock.patch('awx.conf.views.clear_setting_cache'),
|
||||
):
|
||||
response = api_request(
|
||||
'patch',
|
||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}),
|
||||
data={'FOO_LIST': ['HTTP_X_FORWARDED_FOR', 'https://example.com']},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data['FOO_LIST'] == ['HTTP_X_FORWARDED_FOR', 'https://example.com']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_setting_singleton_skips_task_env_shell_expansion(api_request, enforce_clean_text):
|
||||
"""AWX_TASK_ENV values may use ${VAR} / $(...) — excluded from CleanText."""
|
||||
with mock.patch('awx.conf.views.clear_setting_cache'):
|
||||
response = api_request(
|
||||
'patch',
|
||||
reverse('api:setting_singleton_detail', kwargs={'category_slug': 'jobs'}),
|
||||
data={'AWX_TASK_ENV': {'PATH': '${PATH}:/opt/bin', 'CUSTOM': '$(echo hi)'}},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data['AWX_TASK_ENV']['PATH'] == '${PATH}:/opt/bin'
|
||||
assert response.data['AWX_TASK_ENV']['CUSTOM'] == '$(echo hi)'
|
||||
|
||||
@@ -235,6 +235,141 @@ class TestInjectTopLevelCleanTextPatterns:
|
||||
assert result['pattern'] == 'FAKE'
|
||||
assert calls == [(sentinel_field, field_info)]
|
||||
|
||||
def test_plain_serializer_fake_meta_gets_tier2(self, monkeypatch):
|
||||
from ansible_base.lib.serializers.mixins import CleanTextMixin
|
||||
from rest_framework import serializers as drf_serializers
|
||||
|
||||
monkeypatch.setattr(validation_patterns, '_get_tier1_pattern', lambda: {'pattern': 'T1', 'description': 'd1', 'flags': 'u', 'normalize': 'NFC'})
|
||||
monkeypatch.setattr(validation_patterns, '_get_tier2_pattern', lambda: {'pattern': 'T2', 'description': 'd2', 'flags': 'i'})
|
||||
|
||||
class FakeOpts:
|
||||
app_label = 'conf'
|
||||
object_name = 'SettingSingleton'
|
||||
|
||||
class FakeModel:
|
||||
_meta = FakeOpts()
|
||||
|
||||
class PlainCleanText(CleanTextMixin, drf_serializers.Serializer):
|
||||
class Meta:
|
||||
model = FakeModel
|
||||
|
||||
excluded_fields = frozenset()
|
||||
name_fields = frozenset({'name'})
|
||||
|
||||
serializer = PlainCleanText()
|
||||
field = drf_serializers.CharField()
|
||||
field.bind('FOO_BAR', serializer)
|
||||
field_info = {'type': 'string'}
|
||||
|
||||
with override_settings(ENHANCED_INPUT_VALIDATION_ENABLED=True):
|
||||
result = validation_patterns.inject_top_level_clean_text_patterns(field, field_info)
|
||||
|
||||
assert result['pattern'] == 'T2'
|
||||
assert result['patternDescription'] == 'd2'
|
||||
assert result['flags'] == 'i'
|
||||
|
||||
def test_plain_serializer_name_field_gets_tier1(self, monkeypatch):
|
||||
from ansible_base.lib.serializers.mixins import CleanTextMixin
|
||||
from rest_framework import serializers as drf_serializers
|
||||
|
||||
monkeypatch.setattr(validation_patterns, '_get_tier1_pattern', lambda: {'pattern': 'T1', 'description': 'd1', 'flags': 'u', 'normalize': 'NFC'})
|
||||
monkeypatch.setattr(validation_patterns, '_get_tier2_pattern', lambda: {'pattern': 'T2', 'description': 'd2', 'flags': 'i'})
|
||||
|
||||
class FakeOpts:
|
||||
app_label = 'main'
|
||||
object_name = 'CopySerializer'
|
||||
|
||||
class FakeModel:
|
||||
_meta = FakeOpts()
|
||||
|
||||
class PlainCleanText(CleanTextMixin, drf_serializers.Serializer):
|
||||
class Meta:
|
||||
model = FakeModel
|
||||
|
||||
excluded_fields = frozenset()
|
||||
name_fields = frozenset({'name'})
|
||||
|
||||
serializer = PlainCleanText()
|
||||
field = drf_serializers.CharField()
|
||||
field.bind('name', serializer)
|
||||
field_info = {'type': 'string'}
|
||||
|
||||
with override_settings(ENHANCED_INPUT_VALIDATION_ENABLED=True):
|
||||
result = validation_patterns.inject_top_level_clean_text_patterns(field, field_info)
|
||||
|
||||
assert result['pattern'] == 'T1'
|
||||
assert result['patternDescription'] == 'd1'
|
||||
assert result['flags'] == 'u'
|
||||
assert result['normalize'] == 'NFC'
|
||||
|
||||
def test_plain_serializer_skips_excluded_fields(self, monkeypatch):
|
||||
from ansible_base.lib.serializers.mixins import CleanTextMixin
|
||||
from rest_framework import serializers as drf_serializers
|
||||
|
||||
monkeypatch.setattr(validation_patterns, '_get_tier2_pattern', lambda: {'pattern': 'T2', 'description': 'd2', 'flags': 'i'})
|
||||
|
||||
class FakeOpts:
|
||||
app_label = 'conf'
|
||||
object_name = 'SettingSingleton'
|
||||
|
||||
class FakeModel:
|
||||
_meta = FakeOpts()
|
||||
|
||||
class PlainCleanText(CleanTextMixin, drf_serializers.Serializer):
|
||||
class Meta:
|
||||
model = FakeModel
|
||||
|
||||
excluded_fields = frozenset({'CUSTOM_LOGIN_INFO'})
|
||||
name_fields = frozenset()
|
||||
|
||||
serializer = PlainCleanText()
|
||||
field = drf_serializers.CharField()
|
||||
field.bind('CUSTOM_LOGIN_INFO', serializer)
|
||||
field_info = {'type': 'string'}
|
||||
|
||||
with override_settings(ENHANCED_INPUT_VALIDATION_ENABLED=True):
|
||||
result = validation_patterns.inject_top_level_clean_text_patterns(field, field_info)
|
||||
|
||||
assert 'pattern' not in result
|
||||
|
||||
def test_plain_serializer_does_not_call_dab_inject(self, monkeypatch):
|
||||
from ansible_base.lib.serializers.mixins import CleanTextMixin
|
||||
from rest_framework import serializers as drf_serializers
|
||||
|
||||
called = []
|
||||
|
||||
def boom(field, field_info):
|
||||
called.append(True)
|
||||
raise AttributeError('should not reach DAB inject')
|
||||
|
||||
monkeypatch.setattr(validation_patterns, '_dab_inject_clean_text_patterns', boom)
|
||||
monkeypatch.setattr(validation_patterns, '_get_tier2_pattern', lambda: {'pattern': 'T2', 'description': 'd2', 'flags': 'i'})
|
||||
monkeypatch.setattr(validation_patterns, '_get_tier1_pattern', lambda: {'pattern': 'T1', 'description': 'd1', 'flags': 'u', 'normalize': 'NFC'})
|
||||
|
||||
class FakeOpts:
|
||||
app_label = 'conf'
|
||||
object_name = 'SettingSingleton'
|
||||
|
||||
class FakeModel:
|
||||
_meta = FakeOpts()
|
||||
|
||||
class PlainCleanText(CleanTextMixin, drf_serializers.Serializer):
|
||||
class Meta:
|
||||
model = FakeModel
|
||||
|
||||
excluded_fields = frozenset()
|
||||
name_fields = frozenset()
|
||||
|
||||
serializer = PlainCleanText()
|
||||
field = drf_serializers.CharField()
|
||||
field.bind('FOO', serializer)
|
||||
|
||||
with override_settings(ENHANCED_INPUT_VALIDATION_ENABLED=True):
|
||||
result = validation_patterns.inject_top_level_clean_text_patterns(field, {'type': 'string'})
|
||||
|
||||
assert called == []
|
||||
assert result['pattern'] == 'T2'
|
||||
|
||||
|
||||
class TestBuildSurveySpecOptionsSchema:
|
||||
def test_injects_patterns_when_toggle_on(self, fake_tier2_pattern):
|
||||
|
||||
Reference in New Issue
Block a user