From 7112da9cdcc85882232049ffef83c2165e142c23 Mon Sep 17 00:00:00 2001 From: Gabe Muniz Date: Thu, 9 Feb 2023 12:56:33 -0500 Subject: [PATCH] Various validations for const. inv. serialization - prevent constructed inventory host,group,inventory_source creation - disable deleting constructed inventory hosts - remove the ability to add constructed inventory sources - remove ability to add constructed inventories to constructed inventories - block updates to constructed source type - added tests for group/host/source creation --- awx/api/serializers.py | 16 +++-- awx/api/views/__init__.py | 2 + awx/api/views/inventory.py | 9 +++ .../test_inventory_input_constructed.py | 70 +++++++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 awx/main/tests/functional/test_inventory_input_constructed.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c898bf754f..d02a486f5c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1933,8 +1933,8 @@ class HostSerializer(BaseSerializerWithVariables): return value def validate_inventory(self, value): - if value.kind == 'smart': - raise serializers.ValidationError({"detail": _("Cannot create Host for Smart Inventory")}) + if value.kind in ('constructed', 'smart'): + raise serializers.ValidationError({"detail": _("Cannot create Host for Smart or Constructed Inventories")}) return value def validate_variables(self, value): @@ -2032,8 +2032,8 @@ class GroupSerializer(BaseSerializerWithVariables): return value def validate_inventory(self, value): - if value.kind == 'smart': - raise serializers.ValidationError({"detail": _("Cannot create Group for Smart Inventory")}) + if value.kind in ('constructed', 'smart'): + raise serializers.ValidationError({"detail": _("Cannot create Group for Smart or Constructed Inventories")}) return value def to_representation(self, obj): @@ -2339,8 +2339,8 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt return value def validate_inventory(self, value): - if value and value.kind == 'smart': - raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")}) + if value and value.kind in ('constructed', 'smart'): + raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart or Constructed Inventories")}) return value # TODO: remove when old 'credential' fields are removed @@ -2361,6 +2361,8 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt obj = super(InventorySourceSerializer, self).update(obj, validated_data) if deprecated_fields: self._update_deprecated_fields(deprecated_fields, obj) + if obj.source == 'constructed': + raise serializers.ValidationError({'error': _("Cannot edit source of type constructed.")}) return obj # TODO: remove when old 'credential' fields are removed @@ -2387,6 +2389,8 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt if get_field_from_model_or_attrs('source') == 'scm': if ('source' in attrs or 'source_project' in attrs) and get_field_from_model_or_attrs('source_project') is None: raise serializers.ValidationError({"source_project": _("Project required for scm type sources.")}) + elif (get_field_from_model_or_attrs('source') == 'constructed') and (self.instance and self.instance.source != 'constructed'): + raise serializers.ValidationError({"Error": _('constructed not a valid source for inventory')}) else: redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path', 'scm_branch'])) if redundant_scm_fields: diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index c1e99a4002..d023f92984 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -1610,6 +1610,8 @@ class HostDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): def delete(self, request, *args, **kwargs): if self.get_object().inventory.pending_deletion: return Response({"error": _("The inventory for this host is already being deleted.")}, status=status.HTTP_400_BAD_REQUEST) + if self.get_object().inventory.kind == 'constructed': + return Response({"error": _("Delete constructed inventory hosts from input inventory.")}, status=status.HTTP_400_BAD_REQUEST) return super(HostDetail, self).delete(request, *args, **kwargs) diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py index 64550e11c5..453f9c6072 100644 --- a/awx/api/views/inventory.py +++ b/awx/api/views/inventory.py @@ -115,6 +115,15 @@ class InventoryInputInventoriesList(SubListAttachDetachAPIView): parent_model = Inventory relationship = 'input_inventories' + # Specifically overriding the post method on this view to disallow constructed inventories as input inventories + def post(self, request, *args, **kwargs): + obj = Inventory.objects.get(id=request.data.get('id')) + if obj.kind == 'constructed': + return Response( + dict(error=_('You cannot add a constructed inventory to another constructed inventory.')), status=status.HTTP_405_METHOD_NOT_ALLOWED + ) + return super(InventoryInputInventoriesList, self).post(request, *args, **kwargs) + class InventoryActivityStreamList(SubListAPIView): model = ActivityStream diff --git a/awx/main/tests/functional/test_inventory_input_constructed.py b/awx/main/tests/functional/test_inventory_input_constructed.py new file mode 100644 index 0000000000..e677d46f46 --- /dev/null +++ b/awx/main/tests/functional/test_inventory_input_constructed.py @@ -0,0 +1,70 @@ +import pytest +from awx.main.models import Inventory +from awx.api.versioning import reverse + + +@pytest.fixture +def constructed_inventory(organization): + """ + creates a new constructed inventory source + """ + return Inventory.objects.create(name='dummy2', kind='constructed', organization=organization) + + +@pytest.mark.django_db +def test_constructed_inventory_post(post, organization, admin_user): + inventory1 = Inventory.objects.create(name='dummy1', kind='constructed', organization=organization) + inventory2 = Inventory.objects.create(name='dummy2', kind='constructed', organization=organization) + resp = post( + url=reverse('api:inventory_input_inventories', kwargs={'pk': inventory1.pk}), + data={'id': inventory2.pk}, + user=admin_user, + expect=405, + ) + assert resp.status_code == 405 + + +@pytest.mark.django_db +def test_add_constructed_inventory_source(post, admin_user, constructed_inventory): + resp = post( + url=reverse('api:inventory_inventory_sources_list', kwargs={'pk': constructed_inventory.pk}), + data={'name': 'dummy1', 'source': 'constructed'}, + user=admin_user, + expect=400, + ) + assert resp.status_code == 400 + + +@pytest.mark.django_db +def test_add_constructed_inventory_host(post, admin_user, constructed_inventory): + resp = post( + url=reverse('api:inventory_hosts_list', kwargs={'pk': constructed_inventory.pk}), + data={'name': 'dummy1'}, + user=admin_user, + expect=400, + ) + assert resp.status_code == 400 + + +@pytest.mark.django_db +def test_add_constructed_inventory_group(post, admin_user, constructed_inventory): + resp = post( + reverse('api:inventory_groups_list', kwargs={'pk': constructed_inventory.pk}), + data={'name': 'group-test'}, + user=admin_user, + expect=400, + ) + assert resp.status_code == 400 + + +@pytest.mark.django_db +def test_edit_constructed_inventory_source(patch, admin_user, inventory): + inventory.inventory_sources.create(name="dummysrc", source="constructed") + inv_id = inventory.inventory_sources.get(name='dummysrc').id + resp = patch( + reverse('api:inventory_source_detail', kwargs={'pk': inv_id}), + data={'description': inventory.name}, + user=admin_user, + expect=400, + ) + assert resp.status_code == 400