Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/aap_eda/api/metadata.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
try:
from ansible_base.lib.metadata import inject_clean_text_patterns
except ImportError: # pragma: no cover - DAB without AAP-85987
inject_clean_text_patterns = None
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.utils.encoding import force_str
Expand All @@ -23,6 +27,8 @@ class EDAMetadata(metadata.SimpleMetadata):

def get_field_info(self, field):
field_info = super().get_field_info(field)
if inject_clean_text_patterns is not None:
field_info = inject_clean_text_patterns(field, field_info)

for attr in ADDITIONAL_ATTRS:
value = getattr(field, attr, None)
Expand Down
57 changes: 56 additions & 1 deletion src/aap_eda/api/serializers/credential_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.

try:
from ansible_base.lib.metadata import get_tier2_pattern, validation_enabled
except ImportError: # pragma: no cover - DAB without AAP-85987
get_tier2_pattern = None
validation_enabled = None

try:
from ansible_base.lib.serializers.mixins import CleanTextMixin
except ImportError: # pragma: no cover - DAB without AAP-85987
# Provide a no-op stand-in so the class definition is valid
class CleanTextMixin:
pass
from rest_framework import serializers

from aap_eda.core import models, validators
Expand All @@ -37,8 +49,51 @@ class Meta:
*read_only_fields,
]

def to_representation(self, instance):
data = super().to_representation(instance)
inputs = data.get("inputs")
if validation_enabled is not None and validation_enabled() and isinstance(inputs, dict):
data["inputs"] = _with_field_patterns(inputs)
return data


def _with_field_patterns(inputs: dict) -> dict:
"""Return a copy of the inputs schema with patterns for string fields.

CleanTextMixin (from DAB) enforces free-text validation rules on
serializer string fields at write time (when the ENHANCED_INPUT_VALIDATION_ENABLED setting is turned on). Only non-secret "string"
sub-fields get a pattern here, since those are the only ones its
JSON sub-key validation applies to; secret and boolean fields are
left untouched.
"""
Comment thread
daphnemaeve marked this conversation as resolved.
fields = inputs.get("fields")
if not isinstance(fields, list):
return inputs

if get_tier2_pattern is None:
return inputs
pattern = get_tier2_pattern()
new_fields = [
{
**field,
"pattern": pattern["pattern"],
"pattern_description": pattern["description"],
}
if isinstance(field, dict)
and field.get("type") == "string"
and not field.get("secret")
else field
for field in fields
]
return {**inputs, "fields": new_fields}

class CredentialTypeCreateSerializer(
CleanTextMixin, serializers.ModelSerializer
):
# injectors commonly contain Jinja2 template syntax, so it is excluded
# from free-text checks.
excluded_fields = frozenset({"injectors"})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this change live in this PR?
I also see it in #1660.


class CredentialTypeCreateSerializer(serializers.ModelSerializer):
inputs = serializers.JSONField(
required=False,
default=dict,
Expand Down
90 changes: 90 additions & 0 deletions tests/integration/api/test_credential_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from unittest.mock import patch

import pytest
from django.test import override_settings
from pytest_lazyfixture import lazy_fixture
from rest_framework import status
from rest_framework.test import APIClient
Expand All @@ -23,6 +24,13 @@
from aap_eda.core.utils.credentials import SUPPORTED_KEYS_IN_INJECTORS
from tests.integration.constants import api_url_v1

try:
from ansible_base.lib.metadata import get_tier2_pattern as _get_tier2_pattern
_has_dab_validation_metadata = True
except ImportError:
_get_tier2_pattern = None
_has_dab_validation_metadata = False

INPUT = {
"fields": [
{
Expand Down Expand Up @@ -1267,3 +1275,85 @@ def test_eda_rule_engine_credential_validates_required_fields(

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "postgres_db_name" in str(response.data)


@pytest.mark.django_db
class TestCredentialTypeValidationPatterns:
"""AAP-87587: pattern/pattern_description injection for JSON sub-keys."""

@pytest.mark.skipif(not _has_dab_validation_metadata, reason="DAB validation metadata not available (AAP-85987)")
@override_settings(ENHANCED_INPUT_VALIDATION_ENABLED=True)
def test_patterns_present_when_toggle_on(
self,
superuser_client: APIClient,
credential_type: models.CredentialType,
):
get_tier2_pattern = _get_tier2_pattern

response = superuser_client.get(
f"{api_url_v1}/credential-types/{credential_type.id}/"
)
assert response.status_code == status.HTTP_200_OK

pattern = get_tier2_pattern()
fields_by_id = {
field["id"]: field for field in response.data["inputs"]["fields"]
}

username_field = fields_by_id["username"]
assert username_field["pattern"] == pattern["pattern"]
assert username_field["pattern_description"] == pattern["description"]

# secret fields are excluded even though they are also type "string"
password_field = fields_by_id["password"]
assert "pattern" not in password_field
assert "pattern_description" not in password_field

@override_settings(ENHANCED_INPUT_VALIDATION_ENABLED=False)
def test_patterns_absent_when_toggle_off(
self,
superuser_client: APIClient,
credential_type: models.CredentialType,
):
response = superuser_client.get(
f"{api_url_v1}/credential-types/{credential_type.id}/"
)
assert response.status_code == status.HTTP_200_OK

for field in response.data["inputs"]["fields"]:
assert "pattern" not in field
assert "pattern_description" not in field


@pytest.mark.django_db
class TestCredentialTypeOptionsValidationPatterns:
"""EDAMetadata wires DAB's top-level OPTIONS pattern injection.

EDA overrides DEFAULT_METADATA_CLASS with its own EDAMetadata, so DAB's
CleanTextMetadata never runs; EDAMetadata.get_field_info() must call
inject_clean_text_patterns() itself for CleanTextMixin serializers to
advertise a pattern on OPTIONS, same as any other DAB consumer.
"""

@pytest.mark.skipif(not _has_dab_validation_metadata, reason="DAB validation metadata not available (AAP-85987)")
@override_settings(ENHANCED_INPUT_VALIDATION_ENABLED=True)
def test_options_includes_pattern_when_toggle_on(
self, superuser_client: APIClient
):
response = superuser_client.options(f"{api_url_v1}/credential-types/")
assert response.status_code == status.HTTP_200_OK

description_field = response.data["actions"]["POST"]["description"]
assert "pattern" in description_field
assert "patternDescription" in description_field

@override_settings(ENHANCED_INPUT_VALIDATION_ENABLED=False)
def test_options_excludes_pattern_when_toggle_off(
self, superuser_client: APIClient
):
response = superuser_client.options(f"{api_url_v1}/credential-types/")
assert response.status_code == status.HTTP_200_OK

description_field = response.data["actions"]["POST"]["description"]
assert "pattern" not in description_field
assert "patternDescription" not in description_field
Loading