From b1a33869dc901c408035726e825c56e3fb2b5dbf Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 19 Feb 2019 16:08:13 -0500 Subject: [PATCH] convey OpenStack verify_ssl defaults in the CredentialType schema --- awx/main/fields.py | 9 +++ awx/main/models/credential/__init__.py | 9 ++- .../tests/functional/api/test_credential.py | 34 ++++++++++ .../functional/api/test_credential_type.py | 68 +++++++++++++++++++ awx/main/tests/unit/test_tasks.py | 7 +- .../lib/components/input/base.controller.js | 4 +- docs/custom_credential_types.md | 6 ++ 7 files changed, 132 insertions(+), 5 deletions(-) diff --git a/awx/main/fields.py b/awx/main/fields.py index 832fa02f1a..f8a20738ef 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -671,6 +671,7 @@ class CredentialTypeInputField(JSONSchemaField): 'multiline': {'type': 'boolean'}, 'secret': {'type': 'boolean'}, 'ask_at_runtime': {'type': 'boolean'}, + 'default': {}, }, 'additionalProperties': False, 'required': ['id', 'label'], @@ -714,6 +715,14 @@ class CredentialTypeInputField(JSONSchemaField): # If no type is specified, default to string field['type'] = 'string' + if 'default' in field: + default = field['default'] + _type = {'string': str, 'boolean': bool}[field['type']] + if type(default) != _type: + raise django_exceptions.ValidationError( + _('{} is not a {}').format(default, field['type']) + ) + for key in ('choices', 'multiline', 'format', 'secret',): if key in field and field['type'] != 'string': raise django_exceptions.ValidationError( diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index c487d79fbc..12bfe6efe8 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -445,6 +445,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): try: return decrypt_field(self, field_name) except AttributeError: + for field in self.credential_type.inputs.get('fields', []): + if field['id'] == field_name and 'default' in field: + return field['default'] if 'default' in kwargs: return kwargs['default'] raise AttributeError @@ -452,6 +455,9 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): return self.inputs[field_name] if 'default' in kwargs: return kwargs['default'] + for field in self.credential_type.inputs.get('fields', []): + if field['id'] == field_name and 'default' in field: + return field['default'] raise AttributeError(field_name) def has_input(self, field_name): @@ -973,7 +979,8 @@ ManagedCredentialType( }, { 'id': 'verify_ssl', 'label': ugettext_noop('Verify SSL'), - 'type': 'boolean' + 'type': 'boolean', + 'default': True, }], 'required': ['username', 'password', 'host', 'project'] } diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index a031114269..98e4a5b0a7 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -1353,6 +1353,40 @@ def test_openstack_create_ok(post, organization, admin, version, params): assert response.status_code == 201 +@pytest.mark.django_db +@pytest.mark.parametrize('verify_ssl, expected', [ + [None, True], + [True, True], + [False, False], +]) +def test_openstack_verify_ssl(get, post, organization, admin, verify_ssl, expected): + openstack = CredentialType.defaults['openstack']() + openstack.save() + inputs = { + 'username': 'some_user', + 'password': 'some_password', + 'project': 'some_project', + 'host': 'some_host', + } + if verify_ssl is not None: + inputs['verify_ssl'] = verify_ssl + params = { + 'credential_type': openstack.id, + 'inputs': inputs, + 'name': 'Best credential ever', + 'organization': organization.id + } + response = post( + reverse('api:credential_list', kwargs={'version': 'v2'}), + params, + admin + ) + assert response.status_code == 201 + + cred = Credential.objects.get(pk=response.data['id']) + assert cred.get_input('verify_ssl') == expected + + @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ ['v1', {}], diff --git a/awx/main/tests/functional/api/test_credential_type.py b/awx/main/tests/functional/api/test_credential_type.py index e05649ed98..730b3b03d3 100644 --- a/awx/main/tests/functional/api/test_credential_type.py +++ b/awx/main/tests/functional/api/test_credential_type.py @@ -271,6 +271,74 @@ def test_create_with_required_inputs(get, post, admin): assert required == ['api_token'] +@pytest.mark.django_db +@pytest.mark.parametrize('default, status_code', [ + ['some default string', 201], + [None, 400], + [True, 400], + [False, 400], +]) +@pytest.mark.parametrize('secret', [True, False]) +def test_create_with_default_string(get, post, admin, default, status_code, secret): + response = post(reverse('api:credential_type_list'), { + 'kind': 'cloud', + 'name': 'MyCloud', + 'inputs': { + 'fields': [{ + 'id': 'api_token', + 'label': 'API Token', + 'type': 'string', + 'secret': secret, + 'default': default, + }], + 'required': ['api_token'], + }, + 'injectors': {} + }, admin) + assert response.status_code == status_code + if status_code == 201: + cred = Credential( + credential_type=CredentialType.objects.get(pk=response.data['id']), + name='My Custom Cred' + ) + assert cred.get_input('api_token') == default + elif status_code == 400: + assert "{} is not a string".format(default) in json.dumps(response.data) + + +@pytest.mark.django_db +@pytest.mark.parametrize('default, status_code', [ + ['some default string', 400], + [None, 400], + [True, 201], + [False, 201], +]) +def test_create_with_default_bool(get, post, admin, default, status_code): + response = post(reverse('api:credential_type_list'), { + 'kind': 'cloud', + 'name': 'MyCloud', + 'inputs': { + 'fields': [{ + 'id': 'api_token', + 'label': 'API Token', + 'type': 'boolean', + 'default': default, + }], + 'required': ['api_token'], + }, + 'injectors': {} + }, admin) + assert response.status_code == status_code + if status_code == 201: + cred = Credential( + credential_type=CredentialType.objects.get(pk=response.data['id']), + name='My Custom Cred' + ) + assert cred.get_input('api_token') == default + elif status_code == 400: + assert "{} is not a boolean".format(default) in json.dumps(response.data) + + @pytest.mark.django_db @pytest.mark.parametrize('inputs', [ True, diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index a40b235b18..26a1464f5d 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -106,7 +106,7 @@ def test_safe_env_returns_new_copy(): @pytest.mark.parametrize("source,expected", [ - (False, False), (True, True) + (None, True), (False, False), (True, True) ]) def test_openstack_client_config_generation(mocker, source, expected): update = tasks.RunInventoryUpdate() @@ -116,9 +116,10 @@ def test_openstack_client_config_generation(mocker, source, expected): 'username': 'demo', 'password': 'secrete', 'project': 'demo-project', - 'domain': 'my-demo-domain', - 'verify_ssl': source, + 'domain': 'my-demo-domain' } + if source is not None: + inputs['verify_ssl'] = source credential = Credential(pk=1, credential_type=credential_type, inputs=inputs) cred_method = mocker.Mock(return_value=credential) diff --git a/awx/ui/client/lib/components/input/base.controller.js b/awx/ui/client/lib/components/input/base.controller.js index 107815e49e..37d7c467f7 100644 --- a/awx/ui/client/lib/components/input/base.controller.js +++ b/awx/ui/client/lib/components/input/base.controller.js @@ -20,7 +20,7 @@ function BaseInputController (strings) { scope.state._displayPromptOnLaunch = true; } - if (scope.state._value) { + if (typeof scope.state._value !== 'undefined') { scope.state._edit = true; scope.state._preEditValue = scope.state._value; @@ -37,6 +37,8 @@ function BaseInputController (strings) { scope.state._isBeingReplaced = false; scope.state._activeModel = '_displayValue'; } + } else if (typeof scope.state.default !== 'undefined') { + scope.state._value = scope.state.default; } form.register(type, scope); diff --git a/docs/custom_credential_types.md b/docs/custom_credential_types.md index 1192b6799c..64c1143aa2 100644 --- a/docs/custom_credential_types.md +++ b/docs/custom_credential_types.md @@ -136,6 +136,12 @@ ordered fields for that type: "multiline": false # if true, the field should be rendered # as multi-line for input entry # (only applicable to `type=string`) + "default": "default value" # optional, can be used to provide a + # default value if the field is left empty + # when creating a credential of this type + # credential forms will use this value + # as a prefill when making credentials of + # this type },{ # field 2... },{