mirror of
https://github.com/ansible/awx.git
synced 2026-10-31 14:36:17 -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)
|
||||
|
||||
Reference in New Issue
Block a user