From 61846e88ca79ce5881203fba1d8bb69736c70f83 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 14 Jun 2021 22:51:36 -0400 Subject: [PATCH] Add validator for ee image field name awxkit default ee image name is now a fixed valid (but bogus) name, rather than random unicode --- .../0147_validate_ee_image_field.py | 24 +++++ awx/main/models/execution_environments.py | 2 + awx/main/tests/unit/test_validators.py | 37 ++++++++ awx/main/validators.py | 92 +++++++++++++++++++ .../api/pages/execution_environments.py | 2 +- 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 awx/main/migrations/0147_validate_ee_image_field.py diff --git a/awx/main/migrations/0147_validate_ee_image_field.py b/awx/main/migrations/0147_validate_ee_image_field.py new file mode 100644 index 0000000000..84e0500986 --- /dev/null +++ b/awx/main/migrations/0147_validate_ee_image_field.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.16 on 2021-06-15 02:49 + +import awx.main.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0146_add_insights_inventory'), + ] + + operations = [ + migrations.AlterField( + model_name='executionenvironment', + name='image', + field=models.CharField( + help_text='The full image location, including the container registry, image name, and version tag.', + max_length=1024, + validators=[awx.main.validators.validate_container_image_name], + verbose_name='image location', + ), + ), + ] diff --git a/awx/main/models/execution_environments.py b/awx/main/models/execution_environments.py index 656cd97bf0..35af930bf2 100644 --- a/awx/main/models/execution_environments.py +++ b/awx/main/models/execution_environments.py @@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from awx.api.versioning import reverse from awx.main.models.base import CommonModel +from awx.main.validators import validate_container_image_name __all__ = ['ExecutionEnvironment'] @@ -31,6 +32,7 @@ class ExecutionEnvironment(CommonModel): max_length=1024, verbose_name=_('image location'), help_text=_("The full image location, including the container registry, image name, and version tag."), + validators=[validate_container_image_name], ) managed_by_tower = models.BooleanField(default=False, editable=False) credential = models.ForeignKey( diff --git a/awx/main/tests/unit/test_validators.py b/awx/main/tests/unit/test_validators.py index 24c45ca4ef..925ea64335 100644 --- a/awx/main/tests/unit/test_validators.py +++ b/awx/main/tests/unit/test_validators.py @@ -4,6 +4,7 @@ from awx.main.validators import ( validate_certificate, validate_ssh_private_key, vars_validate_or_raise, + validate_container_image_name, ) from awx.main.tests.data.ssh import ( TEST_SSH_RSA1_KEY_DATA, @@ -163,3 +164,39 @@ def test_valid_vars(var_str): def test_invalid_vars(var_str): with pytest.raises(RestValidationError): vars_validate_or_raise(var_str) + + +@pytest.mark.parametrize( + ("image_name", "is_valid"), + [ + ("localhost", True), + ("short", True), + ("simple/name", True), + ("ab/ab/ab/ab", True), + ("foo.com/", False), + ("", False), + ("localhost/foo", True), + ("3asdasdf3", True), + ("xn--7o8h.com/myimage", True), + ("Asdf.com/foo/bar", True), + ("Foo/FarB", False), + ("registry.com:8080/myapp:tag", True), + ("registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True), + ("registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True), + ("registry.com:8080/myapp@sha256:badbadbadbad", False), + ("registry.com:8080/myapp:invalid~tag", False), + ("bad_hostname.com:8080/myapp:tag", False), + ("localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True), + ("localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True), + ("localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", False), + ("localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True), + ("registry.com:8080/myapp@bad", False), + ("registry.com:8080/myapp@2bad", False), + ], +) +def test_valid_container_image_name(image_name, is_valid): + if is_valid: + validate_container_image_name(image_name) + else: + with pytest.raises(ValidationError): + validate_container_image_name(image_name) diff --git a/awx/main/validators.py b/awx/main/validators.py index 3c26922c37..872eabafdc 100644 --- a/awx/main/validators.py +++ b/awx/main/validators.py @@ -195,3 +195,95 @@ def vars_validate_or_raise(vars_str): return vars_str except ParseError as e: raise RestValidationError(str(e)) + + +def validate_container_image_name(value): + """ + from https://github.com/distribution/distribution/blob/af8ac809336c2316c81b08605d92d94f8670ad15/reference/reference.go#L4 + + Grammar + + reference := name [ ":" tag ] [ "@" digest ] + name := [domain '/'] path-component ['/' path-component]* + domain := domain-component ['.' domain-component]* [':' port-number] + domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ + port-number := /[0-9]+/ + path-component := alpha-numeric [separator alpha-numeric]* + alpha-numeric := /[a-z0-9]+/ + separator := /[_.]|__|[-]*/ + + tag := /[\w][\w.-]{0,127}/ + + digest := digest-algorithm ":" digest-hex + digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]* + digest-algorithm-separator := /[+.-_]/ + digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/ + digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value + + The regex below is the printed value of the following GO variable, which represents the 'reference' line above, i.e. the full image name + tag or digest + https://github.com/distribution/distribution/blob/af8ac809336c2316c81b08605d92d94f8670ad15/reference/regexp.go#L72 + """ + # fmt: off + regex = re.compile(r""" + ^ + ( # name + (?: # domain (optional) + (?: + [a-zA-Z0-9] # domain-component + |[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9] + ) + (?: + (?: + \. # additional domain-components, separated by a dot + (?: + [a-zA-Z0-9] + |[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9] + ) + )+ + )? + (?::[0-9]+)? # port number + / # domain should end in slash + )? + [a-z0-9]+ # path-component + (?: + (?: + (?: + [_.]|__|[-]* # path-components can contain separators + ) + [a-z0-9]+ + )+ + )? + (?: + (?: + / # additional path-components, separated by a / + [a-z0-9]+ + (?: + (?: + (?: + [_.]|__|[-]* + ) + [a-z0-9]+ + )+ + )? + )+ + )? + ) # name end + (?: + : # tag (optional) + ([\w][\w.-]{0,127}) # tag limited to 128 characters + )? + (?: + @ # digest (optional) + ( + [A-Za-z][A-Za-z0-9]* # digest-algorithm-component, e.g. sha256 + (?: # additional digest-alorithm components, separated by [-_+.] + [-_+.][A-Za-z][A-Za-z0-9]* + )* + [:][0-9a-fA-F]{32,} # digest-hex + ) + )? + $ + """, re.VERBOSE) + # fmt: on + if not regex.fullmatch(value): + raise ValidationError(_(f"The container image name {value} is not valid")) diff --git a/awxkit/awxkit/api/pages/execution_environments.py b/awxkit/awxkit/api/pages/execution_environments.py index 1df43f90ee..3f6641fba3 100644 --- a/awxkit/awxkit/api/pages/execution_environments.py +++ b/awxkit/awxkit/api/pages/execution_environments.py @@ -40,7 +40,7 @@ class ExecutionEnvironment(HasCreate, HasCopy, base.Base): def payload(self, name='', image=None, organization=None, credential=None, pull='', **kwargs): payload = PseudoNamespace( name=name or "EE - {}".format(random_title()), - image=image or random_title(10), + image=image or "example.invalid/component:tagname", organization=organization.id if organization else None, credential=credential.id if credential else None, pull=pull,