Skip to content

Commit f58e2f3

Browse files
committed
disallow editing of infra type on instances in api, check in model save as well
1 parent 46608e6 commit f58e2f3

4 files changed

Lines changed: 72 additions & 3 deletions

File tree

dojo/cicd_infrastructure/api/serializers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ class CICDInfrastructureSerializer(serializers.ModelSerializer):
77
class Meta:
88
model = CICDInfrastructure
99
fields = "__all__"
10+
11+
def get_fields(self):
12+
fields = super().get_fields()
13+
if self.instance is not None:
14+
# Disallow editing of infra type on an instance; see the matching comment on CICDInfrastructure#save()
15+
fields["infrastructure_type"].read_only = True
16+
return fields

dojo/cicd_infrastructure/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.core.exceptions import ValidationError
12
from django.db import models
23
from django.utils.translation import gettext as _
34

@@ -20,3 +21,15 @@ class Meta:
2021

2122
def __str__(self):
2223
return self.name
24+
25+
def save(self, *args, **kwargs):
26+
if self.pk:
27+
# Disallow editing of the infra type on an instance; engagement CICD FKs are scoped by infrastructure_type
28+
# via limit_choices_to (build_server/scm_server/orchestration), so changing the type would create a
29+
# semantic conflict between an engagement and this object.
30+
current_type = type(self).objects.filter(pk=self.pk).values_list("infrastructure_type", flat=True).first()
31+
if current_type is not None and current_type != self.infrastructure_type:
32+
raise ValidationError(
33+
{"infrastructure_type": _("infrastructure_type cannot be changed once set.")},
34+
)
35+
super().save(*args, **kwargs)

dojo/cicd_infrastructure/ui/forms.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,5 @@ class Meta:
1111
def __init__(self, *args, **kwargs):
1212
super().__init__(*args, **kwargs)
1313
if self.instance.pk:
14-
# Disallow editing of the infra type on an instance; engagement CICD FKs are scoped by infrastructure_type
15-
# via limit_choices_to (build_server/scm_server/orchestration), so changing the type would create a
16-
# semantic conflict between an engagement and this object.
14+
# Disallow editing of infra type on an instance; see the matching comment on CICDInfrastructure#save()
1715
self.fields["infrastructure_type"].disabled = True

unittests/test_cicd_infrastructure.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
``test_cicd_infrastructure_migration.py``.
66
"""
77

8+
from django.core.exceptions import ValidationError
89
from django.db import IntegrityError, transaction
910
from django.urls import reverse
1011
from rest_framework.authtoken.models import Token
@@ -45,6 +46,36 @@ def test_description_and_url_default_to_empty_string(self):
4546
self.assertEqual(infra.description, "")
4647
self.assertEqual(infra.url, "")
4748

49+
def test_save_rejects_infrastructure_type_change_on_existing(self):
50+
"""
51+
Model save() guard: infrastructure_type is immutable once an instance exists.
52+
Engagement CICD FKs are scoped by infrastructure_type via limit_choices_to,
53+
so flipping the type would leave any referencing engagement pointing at a row
54+
whose type contradicts the FK's role.
55+
"""
56+
infra = CICDInfrastructure.objects.create(name="Locked", infrastructure_type="build_server")
57+
infra.infrastructure_type = "scm_server"
58+
with self.assertRaises(ValidationError) as ctx:
59+
infra.save()
60+
self.assertIn("infrastructure_type", ctx.exception.message_dict)
61+
# The DB row is untouched.
62+
self.assertEqual(
63+
CICDInfrastructure.objects.get(pk=infra.pk).infrastructure_type,
64+
"build_server",
65+
)
66+
67+
def test_save_allows_changing_other_fields_on_existing(self):
68+
"""name/description/url remain editable post-creation."""
69+
infra = CICDInfrastructure.objects.create(name="Editable", infrastructure_type="build_server")
70+
infra.name = "Renamed"
71+
infra.description = "now has a description"
72+
infra.url = "https://renamed.example.com"
73+
infra.save()
74+
infra.refresh_from_db()
75+
self.assertEqual(infra.name, "Renamed")
76+
self.assertEqual(infra.description, "now has a description")
77+
self.assertEqual(infra.url, "https://renamed.example.com")
78+
4879

4980
# ---------------------------------------------------------------------------
5081
# Form
@@ -167,3 +198,23 @@ def test_non_superuser_cannot_create(self):
167198
)
168199
self.assertEqual(r.status_code, 403, r.content[:1000])
169200
self.assertFalse(CICDInfrastructure.objects.filter(name="NonSuperCreate").exists())
201+
202+
def test_patch_silently_ignores_infrastructure_type_change(self):
203+
"""
204+
Serializer marks infrastructure_type read_only on update, so DRF drops the
205+
incoming value from validated_data. The PATCH succeeds (200) but the type is
206+
unchanged — same defense the form provides for the legacy UI.
207+
"""
208+
existing = CICDInfrastructure.objects.create(
209+
name="ApiLocked", infrastructure_type="build_server",
210+
)
211+
c = self._client_for("admin")
212+
r = c.patch(
213+
reverse("cicd_infrastructure-detail", args=[existing.pk]),
214+
{"infrastructure_type": "scm_server", "description": "still build"},
215+
format="json",
216+
)
217+
self.assertEqual(r.status_code, 200, r.content[:1000])
218+
existing.refresh_from_db()
219+
self.assertEqual(existing.infrastructure_type, "build_server")
220+
self.assertEqual(existing.description, "still build")

0 commit comments

Comments
 (0)