From b911f8bf77915c67015bcbed0ea6cf2bfa4a2e02 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 27 Feb 2019 14:51:48 -0500 Subject: [PATCH] allow creation at /api/v2/credential_input_sources --- awx/api/views/__init__.py | 2 +- awx/api/views/root.py | 1 + awx/main/models/credential/__init__.py | 9 ++++- .../api/test_credential_input_sources.py | 39 +++++++++++++++++-- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 08b875c9a8..50436d8697 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -1453,7 +1453,7 @@ class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView): serializer_class = serializers.CredentialInputSourceSerializer -class CredentialInputSourceList(ListAPIView): +class CredentialInputSourceList(ListCreateAPIView): view_name = _("Credential Input Sources") diff --git a/awx/api/views/root.py b/awx/api/views/root.py index c7ecbbeef5..66bc11b710 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -101,6 +101,7 @@ class ApiVersionRootView(APIView): data['credentials'] = reverse('api:credential_list', request=request) if get_request_version(request) > 1: data['credential_types'] = reverse('api:credential_type_list', request=request) + data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request) data['applications'] = reverse('api:o_auth2_application_list', request=request) data['tokens'] = reverse('api:o_auth2_token_list', request=request) data['inventory'] = reverse('api:inventory_list', request=request) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index edc9de5372..a572b89120 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -1343,8 +1343,13 @@ class CredentialInputSource(PrimordialModel): return self.source_credential def clean_input_field_name(self): - if self.input_field_name not in self.target_credential.credential_type.defined_fields: - raise ValidationError(_('Input field must be defined on target credential.')) + defined_fields = self.target_credential.credential_type.defined_fields + if self.input_field_name not in defined_fields: + raise ValidationError(_( + 'Input field must be defined on target credential (options are {}).'.format( + ', '.join(sorted(defined_fields)) + ) + )) return self.input_field_name def save(self, *args, **kwargs): diff --git a/awx/main/tests/functional/api/test_credential_input_sources.py b/awx/main/tests/functional/api/test_credential_input_sources.py index a4237083c0..35e95a3a39 100644 --- a/awx/main/tests/functional/api/test_credential_input_sources.py +++ b/awx/main/tests/functional/api/test_credential_input_sources.py @@ -77,16 +77,18 @@ def test_associate_credential_input_source_with_invalid_metadata(get, post, admi @pytest.mark.django_db -def test_cannot_create_from_list(get, post, admin, vault_credential, external_credential): +def test_create_from_list(get, post, admin, vault_credential, external_credential): params = { 'source_credential': external_credential.pk, 'target_credential': vault_credential.pk, 'input_field_name': 'vault_password', + 'metadata': {'key': 'some_example_key'}, } assert post(reverse( 'api:credential_input_source_list', kwargs={'version': 'v2'} - ), params, admin).status_code == 405 + ), params, admin).status_code == 201 + assert CredentialInputSource.objects.count() == 1 @pytest.mark.django_db @@ -207,6 +209,37 @@ def test_input_source_detail_rbac(get, post, patch, delete, admin, alice, assert CredentialInputSource.objects.count() == 0 +@pytest.mark.django_db +def test_input_source_create_rbac(get, post, patch, delete, alice, + vault_credential, external_credential, + other_external_credential): + sublist_url = reverse( + 'api:credential_input_source_list', + kwargs={'version': 'v2'} + ) + params = { + 'target_credential': vault_credential.pk, + 'source_credential': external_credential.pk, + 'input_field_name': 'vault_password', + 'metadata': {'key': 'some_key'}, + } + + # alice can't create the inv source because she has access to neither credential + response = post(sublist_url, params, alice) + assert response.status_code == 403 + + # alice still can't because she can't use the source credential + vault_credential.admin_role.members.add(alice) + response = post(sublist_url, params, alice) + assert response.status_code == 403 + + # alice can create an input source if she has permissions on both credentials + external_credential.use_role.members.add(alice) + response = post(sublist_url, params, alice) + assert response.status_code == 201 + assert CredentialInputSource.objects.count() == 1 + + @pytest.mark.django_db def test_input_source_rbac_swap_target_credential(get, post, put, patch, admin, alice, machine_credential, vault_credential, @@ -278,7 +311,7 @@ def test_create_credential_input_source_with_undefined_input_returns_400(post, a } response = post(sublist_url, params, admin) assert response.status_code == 400 - assert response.data['input_field_name'] == ['Input field must be defined on target credential.'] + assert response.data['input_field_name'] == ['Input field must be defined on target credential (options are vault_id, vault_password).'] @pytest.mark.django_db