diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cb3952bd78..cdb334279c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1857,10 +1857,14 @@ class CredentialTypeSerializer(BaseSerializer): if self.instance and self.instance.managed_by_tower: raise serializers.ValidationError( {"detail": _("Modifications not allowed for credential types managed by Tower")}) + if self.instance and self.instance.credentials.exists(): + if 'inputs' in attrs and attrs['inputs'] != self.instance.inputs: + raise serializers.ValidationError( + {"inputs": _("Modifications to inputs are not allowed for credential types that are in use")}) fields = attrs.get('inputs', {}).get('fields', []) for field in fields: if field.get('ask_at_runtime', False): - raise serializers.ValidationError({"detail": _("'ask_at_runtime' is not supported for custom credentials.")}) + raise serializers.ValidationError({"inputs": _("'ask_at_runtime' is not supported for custom credentials.")}) return super(CredentialTypeSerializer, self).validate(attrs) diff --git a/awx/api/views.py b/awx/api/views.py index a2e902c8b9..c353bf5510 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1504,6 +1504,12 @@ class CredentialTypeDetail(RetrieveUpdateDestroyAPIView): new_in_320 = True new_in_api_v2 = True + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + if instance.managed_by_tower or instance.credentials.exists(): + raise PermissionDenied(detail=_("Credential types that are in use cannot be deleted.")) + return super(CredentialTypeDetail, self).destroy(request, *args, **kwargs) + class CredentialList(ListCreateAPIView): diff --git a/awx/main/access.py b/awx/main/access.py index ed5fd5c90d..9030aa846b 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -809,8 +809,6 @@ class CredentialTypeAccess(BaseAccess): - I'm a superuser: I can change when: - I'm a superuser and the type is not "managed by Tower" - I can change/delete when: - - I'm a superuser and the type is not "managed by Tower" ''' model = CredentialType diff --git a/awx/main/tests/functional/api/test_credential_type.py b/awx/main/tests/functional/api/test_credential_type.py index a0b18e966e..86f9ad8d60 100644 --- a/awx/main/tests/functional/api/test_credential_type.py +++ b/awx/main/tests/functional/api/test_credential_type.py @@ -2,7 +2,7 @@ import json import pytest -from awx.main.models.credential import CredentialType +from awx.main.models.credential import CredentialType, Credential from awx.api.versioning import reverse @@ -35,16 +35,55 @@ def test_create_as_unauthorized_xfail(get, post): @pytest.mark.django_db -def test_update_as_unauthorized_xfail(patch): +def test_update_as_unauthorized_xfail(patch, delete): ssh = CredentialType.defaults['ssh']() ssh.save() - response = patch( - reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}), - { - 'name': 'Some Other Name' - } - ) + url = reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}) + response = patch(url, {'name': 'Some Other Name'}) assert response.status_code == 401 + assert delete(url).status_code == 401 + + +@pytest.mark.django_db +def test_update_managed_by_tower_xfail(patch, delete, admin): + ssh = CredentialType.defaults['ssh']() + ssh.save() + url = reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}) + response = patch(url, {'name': 'Some Other Name'}, admin) + assert response.status_code == 400 + assert delete(url, admin).status_code == 403 + + +@pytest.mark.django_db +def test_update_credential_type_in_use_xfail(patch, delete, admin): + ssh = CredentialType.defaults['ssh']() + ssh.managed_by_tower = False + ssh.save() + Credential(credential_type=ssh, name='My SSH Key').save() + + url = reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}) + response = patch(url, {'name': 'Some Other Name'}, admin) + assert response.status_code == 200 + + url = reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}) + response = patch(url, {'inputs': {}}, admin) + assert response.status_code == 400 + + assert delete(url, admin).status_code == 403 + + +@pytest.mark.django_db +def test_update_credential_type_success(get, patch, delete, admin): + ssh = CredentialType.defaults['ssh']() + ssh.managed_by_tower = False + ssh.save() + + url = reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}) + response = patch(url, {'name': 'Some Other Name'}, admin) + assert response.status_code == 200 + + assert get(url, admin).data.get('name') == 'Some Other Name' + assert delete(url, admin).status_code == 204 @pytest.mark.django_db