diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 360647b60a..1299ba7aee 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2537,10 +2537,11 @@ class CredentialTypeSerializer(BaseSerializer): class CredentialSerializer(BaseSerializer): show_capabilities = ['edit', 'delete', 'copy', 'use'] capabilities_prefetch = ['admin', 'use'] + managed_by_tower = serializers.ReadOnlyField() class Meta: model = Credential - fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud', 'kubernetes') + fields = ('*', 'organization', 'credential_type', 'managed_by_tower', 'inputs', 'kind', 'cloud', 'kubernetes') extra_kwargs = { 'credential_type': { 'label': _('Credential Type'), @@ -2604,6 +2605,13 @@ class CredentialSerializer(BaseSerializer): return summary_dict + def validate(self, attrs): + if self.instance and self.instance.managed_by_tower: + raise PermissionDenied( + detail=_("Modifications not allowed for managed credentials") + ) + return super(CredentialSerializer, self).validate(attrs) + def get_validation_exclusions(self, obj=None): ret = super(CredentialSerializer, self).get_validation_exclusions(obj) for field in ('credential_type', 'inputs'): diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 0bb709fce3..4f436c8f0e 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -1356,6 +1356,13 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView): model = models.Credential serializer_class = serializers.CredentialSerializer + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + if instance.managed_by_tower: + raise PermissionDenied(detail=_("Deletion not allowed for managed credentials")) + return super(CredentialDetail, self).destroy(request, *args, **kwargs) + + class CredentialActivityStreamList(SubListAPIView): diff --git a/awx/main/access.py b/awx/main/access.py index 4f54be6e12..8c4d162a7f 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1103,11 +1103,6 @@ class CredentialTypeAccess(BaseAccess): def can_use(self, obj): return True - def get_method_capability(self, method, obj, parent_obj): - if obj.managed_by_tower: - return False - return super(CredentialTypeAccess, self).get_method_capability(method, obj, parent_obj) - def filtered_queryset(self): return self.model.objects.all() @@ -1182,6 +1177,8 @@ class CredentialAccess(BaseAccess): def get_user_capabilities(self, obj, **kwargs): user_capabilities = super(CredentialAccess, self).get_user_capabilities(obj, **kwargs) user_capabilities['use'] = self.can_use(obj) + if getattr(obj, 'managed_by_tower', False) is True: + user_capabilities['edit'] = user_capabilities['delete'] = False return user_capabilities diff --git a/awx/main/migrations/0118_galaxy_credentials.py b/awx/main/migrations/0118_galaxy_credentials.py index d47f966fec..c469141767 100644 --- a/awx/main/migrations/0118_galaxy_credentials.py +++ b/awx/main/migrations/0118_galaxy_credentials.py @@ -42,5 +42,10 @@ class Migration(migrations.Migration): name='galaxy_credentials', field=awx.main.fields.OrderedManyToManyField(blank=True, related_name='organization_galaxy_credentials', through='main.OrganizationGalaxyCredentialMembership', to='main.Credential'), ), + migrations.AddField( + model_name='credential', + name='managed_by_tower', + field=models.BooleanField(default=False, editable=False), + ), migrations.RunPython(galaxy.migrate_galaxy_settings) ] diff --git a/awx/main/migrations/_galaxy.py b/awx/main/migrations/_galaxy.py index 55166585d9..6341ee640c 100644 --- a/awx/main/migrations/_galaxy.py +++ b/awx/main/migrations/_galaxy.py @@ -32,6 +32,18 @@ def migrate_galaxy_settings(apps, schema_editor): # ...UNLESS this behavior was explicitly disabled via this setting public_galaxy_enabled = False + public_galaxy_credential = Credential( + created=now(), + modified=now(), + name='Ansible Galaxy', + managed_by_tower=True, + credential_type=galaxy_type, + inputs = { + 'url': 'https://galaxy.ansible.com/' + } + ) + public_galaxy_credential.save() + for org in Organization.objects.all(): if private_galaxy_url and private_galaxy_url.value: # If a setting exists for a private Galaxy URL, make a credential for it @@ -106,16 +118,5 @@ def migrate_galaxy_settings(apps, schema_editor): org.galaxy_credentials.add(cred) if public_galaxy_enabled: - # If public Galaxy was enabled, make a credential for it - cred = Credential( - created=now(), - modified=now(), - name='Ansible Galaxy', - organization=org, - credential_type=galaxy_type, - inputs = { - 'url': 'https://galaxy.ansible.com/' - } - ) - cred.save() - org.galaxy_credentials.add(cred) + # If public Galaxy was enabled, associate it to the org + org.galaxy_credentials.add(public_galaxy_credential) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 9756fd1639..df12177aae 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -96,6 +96,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): help_text=_('Specify the type of credential you want to create. Refer ' 'to the Ansible Tower documentation for details on each type.') ) + managed_by_tower = models.BooleanField( + default=False, + editable=False + ) organization = models.ForeignKey( 'Organization', null=True, diff --git a/awx/main/tests/functional/test_galaxy_credential_migration.py b/awx/main/tests/functional/test_galaxy_credential_migration.py index 1cf008b193..110628e19c 100644 --- a/awx/main/tests/functional/test_galaxy_credential_migration.py +++ b/awx/main/tests/functional/test_galaxy_credential_migration.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType import pytest -from awx.main.models import Organization +from awx.main.models import Credential, Organization from awx.conf.models import Setting from awx.main.migrations import _galaxy as galaxy @@ -78,6 +78,10 @@ def test_multiple_galaxies(): assert creds[1].name == 'Ansible Galaxy' assert creds[1].inputs['url'] == 'https://galaxy.ansible.com/' + public_galaxy_creds = Credential.objects.filter(name='Ansible Galaxy') + assert public_galaxy_creds.count() == 1 + assert public_galaxy_creds.first().managed_by_tower is True + @pytest.mark.django_db def test_fallback_galaxies():