diff --git a/awx/main/fields.py b/awx/main/fields.py index ae0d89848a..f7953f5830 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -420,15 +420,15 @@ def format_ssh_private_key(value): # These end in a unicode-encoded final character that gets double # escaped due to being in a Python 2 bytestring, and that causes # Python's key parsing to barf. Detect this issue and correct it. - if value == '$encrypted$': - return value + if not value or value == '$encrypted$': + return True if r'\u003d' in value: value = value.replace(r'\u003d', '=') try: validate_ssh_private_key(value) except django_exceptions.ValidationError as e: raise jsonschema.exceptions.FormatError(e.message) - return value + return True class CredentialInputField(JSONSchemaField): @@ -478,6 +478,13 @@ class CredentialInputField(JSONSchemaField): return super(CredentialInputField, self).validate(value, model_instance) + # Backwards compatability: in prior versions, if you submit `null` for + # a credential field value, it just considers the value an empty string + for unset in [key for key, v in model_instance.inputs.items() if not v]: + default_value = model_instance.credential_type.default_for_field(unset) + if default_value is not None: + model_instance.inputs[unset] = default_value + decrypted_values = {} for k, v in value.items(): if all([ diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 7f6b875ae8..938d119faf 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -457,6 +457,13 @@ class CredentialType(CommonModelNameNotUnique): if field.get('ask_at_runtime', False) is True ] + def default_for_field(self, field_id): + for field in self.inputs.get('fields', []): + if field['id'] == field_id: + if 'choices' in field: + return field['choices'][0] + return {'string': '', 'boolean': False}[field['type']] + @classmethod def default(cls, f): func = functools.partial(f, cls) diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index 4a95223c21..4a94777bb8 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -1,3 +1,5 @@ +import itertools + import mock # noqa import pytest @@ -707,6 +709,59 @@ def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, cred assert "'invalid_field' was unexpected" in response.data['inputs'][0] +@pytest.mark.django_db +@pytest.mark.parametrize('field_name, field_value', itertools.product( + ['username', 'password', 'ssh_key_data', 'ssh_key_unlock', 'become_method', 'become_username', 'become_password'], # noqa + ['', None] +)) +def test_nullish_field_data(get, post, organization, admin, field_name, field_value): + ssh = CredentialType.defaults['ssh']() + ssh.save() + params = { + 'name': 'Best credential ever', + 'credential_type': ssh.pk, + 'organization': organization.id, + 'inputs': { + field_name: field_value + } + } + response = post( + reverse('api:credential_list', kwargs={'version': 'v2'}), + params, + admin + ) + assert response.status_code == 201 + + assert Credential.objects.count() == 1 + cred = Credential.objects.all()[:1].get() + assert getattr(cred, field_name) == '' + + +@pytest.mark.django_db +@pytest.mark.parametrize('field_value', ['', None, False]) +def test_falsey_field_data(get, post, organization, admin, field_value): + net = CredentialType.defaults['net']() + net.save() + params = { + 'name': 'Best credential ever', + 'credential_type': net.pk, + 'organization': organization.id, + 'inputs': { + 'authorize': field_value + } + } + response = post( + reverse('api:credential_list', kwargs={'version': 'v2'}), + params, + admin + ) + assert response.status_code == 201 + + assert Credential.objects.count() == 1 + cred = Credential.objects.all()[:1].get() + assert cred.authorize is False + + # # SCM Credentials #