diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 1fb9a43f72..1281351ade 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -4694,6 +4694,8 @@ class InstanceGroupSerializer(BaseSerializer): raise serializers.ValidationError(_('{} is not a valid hostname of an existing instance.').format(instance_name)) if Instance.objects.get(hostname=instance_name).is_isolated(): raise serializers.ValidationError(_('Isolated instances may not be added or removed from instances groups via the API.')) + if self.instance and self.instance.controller_id is not None: + raise serializers.ValidationError(_('Isolated instance group membership may not be managed via the API.')) return value def validate_name(self, value): diff --git a/awx/api/views.py b/awx/api/views.py index c99821e5b8..64e326c4eb 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -192,6 +192,10 @@ class InstanceGroupMembershipMixin(object): def is_valid_relation(self, parent, sub, created=False): if sub.is_isolated(): return {'error': _('Isolated instances may not be added or removed from instances groups via the API.')} + if self.parent_model is InstanceGroup: + ig_obj = self.get_parent_object() + if ig_obj.controller_id is not None: + return {'error': _('Isolated instance group membership may not be managed via the API.')} return None def unattach_validate(self, request): diff --git a/awx/main/tests/functional/api/test_instance_group.py b/awx/main/tests/functional/api/test_instance_group.py index c9a0ad4a28..939db63dc2 100644 --- a/awx/main/tests/functional/api/test_instance_group.py +++ b/awx/main/tests/functional/api/test_instance_group.py @@ -21,6 +21,11 @@ def instance(): return instance +@pytest.fixture +def non_iso_instance(): + return Instance.objects.create(hostname='iamnotanisolatedinstance') + + @pytest.fixture def instance_group(job_factory): ig = InstanceGroup(name="east") @@ -138,7 +143,7 @@ def test_prevent_isolated_instance_added_to_non_isolated_instance_group_via_poli url = reverse("api:instance_group_detail", kwargs={'pk': instance_group.pk}) assert True is instance.is_isolated() - resp = patch(url, {'policy_instance_list': [instance.hostname]}, admin) + resp = patch(url, {'policy_instance_list': [instance.hostname]}, user=admin, expect=400) assert [u"Isolated instances may not be added or removed from instances groups via the API."] == resp.data['policy_instance_list'] assert instance_group.policy_instance_list == [] @@ -150,3 +155,24 @@ def test_prevent_isolated_instance_removal_from_isolated_instance_group(post, ad assert True is instance.is_isolated() resp = post(url, {'disassociate': True, 'id': instance.id}, admin, expect=400) assert u"Isolated instances may not be added or removed from instances groups via the API." == resp.data['error'] + + +@pytest.mark.django_db +def test_prevent_non_isolated_instance_added_to_isolated_instance_group( + post, admin, non_iso_instance, isolated_instance_group): + url = reverse("api:instance_group_instance_list", kwargs={'pk': isolated_instance_group.pk}) + + assert False is non_iso_instance.is_isolated() + resp = post(url, {'associate': True, 'id': non_iso_instance.id}, admin, expect=400) + assert u"Isolated instance group membership may not be managed via the API." == resp.data['error'] + + +@pytest.mark.django_db +def test_prevent_non_isolated_instance_added_to_isolated_instance_group_via_policy_list( + patch, admin, non_iso_instance, isolated_instance_group): + url = reverse("api:instance_group_detail", kwargs={'pk': isolated_instance_group.pk}) + + assert False is non_iso_instance.is_isolated() + resp = patch(url, {'policy_instance_list': [non_iso_instance.hostname]}, user=admin, expect=400) + assert [u"Isolated instance group membership may not be managed via the API."] == resp.data['policy_instance_list'] + assert isolated_instance_group.policy_instance_list == []