Skip to content

Commit 1c948a6

Browse files
authored
fix: add validation of host input for registry credential (#1140)
1 parent ac4be90 commit 1c948a6

File tree

7 files changed

+228
-49
lines changed

7 files changed

+228
-49
lines changed

src/aap_eda/api/serializers/eda_credential.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ def validate(self, data):
120120
)
121121

122122
inputs = data.get("inputs", {})
123-
errors = validate_inputs(credential_type.inputs, inputs)
123+
errors = validate_inputs(
124+
credential_type,
125+
credential_type.inputs,
126+
inputs,
127+
)
124128
if bool(errors):
125129
raise serializers.ValidationError(errors)
126130

@@ -153,7 +157,11 @@ def validate(self, data):
153157
if self.partial and not bool(inputs):
154158
return data
155159

156-
errors = validate_inputs(credential_type.inputs, inputs)
160+
errors = validate_inputs(
161+
credential_type,
162+
credential_type.inputs,
163+
inputs,
164+
)
157165
if bool(errors):
158166
raise serializers.ValidationError(errors)
159167

src/aap_eda/core/utils/credentials.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@
1414

1515
import re
1616
import tempfile
17+
import typing
1718

1819
import gnupg
1920
import jinja2
21+
import validators
2022
import yaml
2123
from django.core.exceptions import ValidationError
2224
from jinja2.nativetypes import NativeTemplate
25+
from rest_framework import serializers
2326

2427
from aap_eda.api.constants import EDA_SERVER_VAULT_LABEL
28+
from aap_eda.core import enums
29+
30+
if typing.TYPE_CHECKING:
31+
from aap_eda.core import models
2532
from aap_eda.core.utils.awx import validate_ssh_private_key
2633

2734
ENCRYPTED_STRING = "$encrypted$"
@@ -80,7 +87,11 @@ def inputs_from_store(inputs: str) -> dict:
8087
return yaml.safe_load(inputs)
8188

8289

83-
def validate_inputs(schema: dict, inputs: dict) -> dict:
90+
def validate_inputs(
91+
credential_type: "models.CredentialType",
92+
schema: dict,
93+
inputs: dict,
94+
) -> dict:
8495
"""Validate user inputs against credential schema.
8596
8697
Sample output:
@@ -137,6 +148,17 @@ def validate_inputs(schema: dict, inputs: dict) -> dict:
137148
else:
138149
errors[display_field] = result
139150

151+
# We apply particular requirements on "host" when it is
152+
# associated with a container registry.
153+
if (
154+
(credential_type.name == enums.DefaultCredentialType.REGISTRY)
155+
and (field == "host")
156+
and user_input
157+
):
158+
result = _validate_registry_host_name(user_input)
159+
if bool(result):
160+
errors[display_field] = result
161+
140162
if field == "gpg_public_key":
141163
result = _validate_gpg_public_key(user_input)
142164
if bool(result):
@@ -278,6 +300,27 @@ def validate_injectors(schema: dict, injectors: dict) -> dict:
278300
return {"injectors": errors} if bool(errors) else {}
279301

280302

303+
def validate_registry_host_name(host: str) -> None:
304+
errors = _validate_registry_host_name(host)
305+
if bool(errors):
306+
raise serializers.ValidationError(f"invalid host name: {host}")
307+
308+
309+
def _validate_registry_host_name(host: str) -> list[str]:
310+
errors = []
311+
312+
# Yes, validators returns (not throws) an exception if the argument doesn't
313+
# pass muster (it returns True otherwise). Consequently we have to check
314+
# the class of the return to know what happened and if it's not validators'
315+
# validation exception raise whatever the heck it is.
316+
validity = validators.hostname(host)
317+
if isinstance(validity, Exception):
318+
if not isinstance(validity, validators.ValidationError):
319+
raise
320+
errors.append("Host format invalid")
321+
return errors
322+
323+
281324
def _get_id_fields(schema: dict) -> list[str]:
282325
fields = schema.get("fields", [])
283326

src/aap_eda/core/validators.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@
1717
import typing as tp
1818
import urllib
1919

20-
import validators
2120
import yaml
2221
from django.conf import settings
2322
from django.utils.translation import gettext_lazy as _
2423
from rest_framework import serializers
2524

2625
from aap_eda.core import enums, models
27-
from aap_eda.core.utils.credentials import validate_schema
26+
from aap_eda.core.utils.credentials import (
27+
validate_registry_host_name,
28+
validate_schema,
29+
)
2830
from aap_eda.core.utils.k8s_service_name import is_rfc_1035_compliant
2931

3032
logger = logging.getLogger(__name__)
@@ -69,8 +71,7 @@ def check_if_de_valid(image_url: str, eda_credential_id: int):
6971
# We split the image url on the first slash into the host and path. The
7072
# path is further split into a name and tag on the rightmost colon.
7173
#
72-
# THe host portion is validated using the validators package and the path
73-
# and tag are validated using the OCI regexes for each.
74+
# The path and tag are validated using the OCI regexes for each.
7475
split = image_url.split("/", 1)
7576
host = split[0]
7677
path = split[1] if len(split) > 1 else None
@@ -81,14 +82,11 @@ def check_if_de_valid(image_url: str, eda_credential_id: int):
8182
% {"image_url": image_url}
8283
)
8384

84-
# Yes, validators returns (not throws) an exception if the argument doesn't
85-
# pass muster (it returns True otherwise). Consequently we have to check
86-
# the class of the return to know what happened and if it's not validators'
87-
# validation exception raise whatever the heck it is.
88-
validity = validators.hostname(host)
89-
if isinstance(validity, Exception):
90-
if not isinstance(validity, validators.ValidationError):
91-
raise
85+
try:
86+
validate_registry_host_name(host)
87+
except serializers.ValidationError:
88+
# We raise our own instance of this exception in order to assert
89+
# control over the format of the message.
9290
raise serializers.ValidationError(
9391
_(
9492
"Image url %(image_url)s is malformed; "

tests/conftest.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
import pytest
1818
from django.conf import settings
1919

20-
from aap_eda.core import models
20+
from aap_eda.core import enums, models
21+
from aap_eda.core.management.commands.create_initial_data import (
22+
CREDENTIAL_TYPES,
23+
populate_credential_types,
24+
)
2125
from aap_eda.settings import default
2226

2327

@@ -46,6 +50,24 @@ def default_organization() -> models.Organization:
4650
)[0]
4751

4852

53+
#################################################################
54+
# DB
55+
#################################################################
56+
@pytest.fixture
57+
def preseed_credential_types(
58+
default_organization: models.Organization,
59+
) -> list[models.CredentialType]:
60+
"""Preseed Credential Types."""
61+
return populate_credential_types(CREDENTIAL_TYPES)
62+
63+
64+
@pytest.fixture
65+
def aap_credential_type(preseed_credential_types):
66+
return models.CredentialType.objects.get(
67+
name=enums.DefaultCredentialType.AAP
68+
)
69+
70+
4971
#################################################################
5072
# Redis
5173
#################################################################

tests/integration/api/test_eda_credential.py

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,67 @@ def test_create_eda_credential_with_gpg_key_data(
206206
assert message.startswith(status_message)
207207

208208

209+
@pytest.mark.parametrize(
210+
("status_code", "hostname"),
211+
[
212+
(
213+
status.HTTP_201_CREATED,
214+
"validname",
215+
),
216+
(
217+
status.HTTP_201_CREATED,
218+
"valid-name",
219+
),
220+
(
221+
status.HTTP_201_CREATED,
222+
"valid.name",
223+
),
224+
(
225+
status.HTTP_400_BAD_REQUEST,
226+
"invalid name",
227+
),
228+
(
229+
status.HTTP_400_BAD_REQUEST,
230+
"invalid/name",
231+
),
232+
(
233+
status.HTTP_400_BAD_REQUEST,
234+
"invalid@name",
235+
),
236+
(
237+
status.HTTP_400_BAD_REQUEST,
238+
"https://invalid.name",
239+
),
240+
],
241+
)
242+
@pytest.mark.django_db
243+
def test_create_registry_eda_credential_with_various_host_names(
244+
admin_client: APIClient,
245+
default_organization: models.Organization,
246+
preseed_credential_types,
247+
status_code,
248+
hostname,
249+
):
250+
credential_type = models.CredentialType.objects.get(
251+
name=enums.DefaultCredentialType.REGISTRY
252+
)
253+
254+
data_in = {
255+
"name": f"eda-credential-{credential_type}",
256+
"inputs": {
257+
"host": hostname,
258+
},
259+
"credential_type_id": credential_type.id,
260+
"organization_id": default_organization.id,
261+
}
262+
response = admin_client.post(
263+
f"{api_url_v1}/eda-credentials/", data=data_in
264+
)
265+
assert response.status_code == status_code
266+
if status_code == status.HTTP_400_BAD_REQUEST:
267+
assert "Host format invalid" in response.data["inputs.host"]
268+
269+
209270
@pytest.mark.parametrize(
210271
("credential_type", "status_code", "key", "error_message"),
211272
[
@@ -551,11 +612,11 @@ def test_partial_update_eda_credential_type_not_changed(
551612
"verify_ssl": True,
552613
},
553614
{
554-
"host": "new host",
615+
"host": "new-host",
555616
"verify_ssl": False,
556617
},
557618
{
558-
"host": "new host",
619+
"host": "new-host",
559620
"username": "user name",
560621
"password": ENCRYPTED_STRING,
561622
"oauth_token": ENCRYPTED_STRING,
@@ -605,12 +666,12 @@ def test_partial_update_eda_credential_type_not_changed(
605666
"verify_ssl": True,
606667
},
607668
{
608-
"host": "new host",
669+
"host": "new-host",
609670
"username": "new user name",
610671
"verify_ssl": False,
611672
},
612673
{
613-
"host": "new host",
674+
"host": "new-host",
614675
"username": "new user name",
615676
"password": ENCRYPTED_STRING,
616677
"verify_ssl": False,
@@ -625,12 +686,12 @@ def test_partial_update_eda_credential_type_not_changed(
625686
"verify_ssl": True,
626687
},
627688
{
628-
"host": "new host",
689+
"host": "new-host",
629690
"username": "new user name",
630691
"password": "password",
631692
},
632693
{
633-
"host": "new host",
694+
"host": "new-host",
634695
"username": "new user name",
635696
"password": ENCRYPTED_STRING,
636697
"verify_ssl": True,

tests/integration/conftest.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@
2727

2828
from aap_eda.core import enums, models
2929
from aap_eda.core.management.commands import create_initial_data
30-
from aap_eda.core.management.commands.create_initial_data import (
31-
CREDENTIAL_TYPES,
32-
populate_credential_types,
33-
)
3430
from aap_eda.core.tasking import Queue, get_redis_client
3531
from aap_eda.core.utils.credentials import inputs_to_store
3632
from aap_eda.services.activation.engine.common import ContainerEngine
@@ -1035,14 +1031,6 @@ def credential_payload(
10351031
}
10361032

10371033

1038-
@pytest.fixture
1039-
def preseed_credential_types(
1040-
default_organization: models.Organization,
1041-
) -> list[models.CredentialType]:
1042-
"""Preseed Credential Types."""
1043-
return populate_credential_types(CREDENTIAL_TYPES)
1044-
1045-
10461034
@pytest.fixture
10471035
def default_gpg_credential(
10481036
default_organization: models.Organization,

0 commit comments

Comments
 (0)