Skip to content

Commit fa218f8

Browse files
committed
fix(models): Enforce unique ACL interface assignment
Fixes the unique constraint for ACLInterfaceAssignment by removing the access_list field from the uniqueness check. Ensures that only one ACL can be assigned per interface and direction. Adds validation tests to prevent multiple ACLs from being assigned in the same direction on the same interface. Fixes #258
1 parent 00a7246 commit fa218f8

File tree

3 files changed

+82
-3
lines changed

3 files changed

+82
-3
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Generated by Django 5.1.7 on 2025-03-26 18:46
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('netbox_acls', '0004_netbox_acls'),
11+
]
12+
13+
operations = [
14+
migrations.AlterModelOptions(
15+
name='accesslist',
16+
options={'ordering': ('assigned_object_type', 'assigned_object_id', 'name')},
17+
),
18+
migrations.AlterModelOptions(
19+
name='aclextendedrule',
20+
options={'ordering': ('access_list', 'index')},
21+
),
22+
migrations.AlterModelOptions(
23+
name='aclinterfaceassignment',
24+
options={'ordering': ('assigned_object_type', 'assigned_object_id', 'access_list', 'direction')},
25+
),
26+
migrations.AlterModelOptions(
27+
name='aclstandardrule',
28+
options={'ordering': ('access_list', 'index')},
29+
),
30+
migrations.AlterUniqueTogether(
31+
name='aclinterfaceassignment',
32+
unique_together=set(),
33+
),
34+
migrations.AlterField(
35+
model_name='accesslist',
36+
name='assigned_object_type',
37+
field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'device')), models.Q(('app_label', 'dcim'), ('model', 'virtualchassis')), models.Q(('app_label', 'virtualization'), ('model', 'virtualmachine')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'),
38+
),
39+
migrations.AlterField(
40+
model_name='aclinterfaceassignment',
41+
name='assigned_object_type',
42+
field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'),
43+
),
44+
migrations.AlterUniqueTogether(
45+
name='aclinterfaceassignment',
46+
unique_together={('assigned_object_type', 'assigned_object_id', 'direction')},
47+
),
48+
]

netbox_acls/models/access_lists.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ class Meta:
133133
unique_together = (
134134
"assigned_object_type",
135135
"assigned_object_id",
136-
"access_list",
137136
"direction",
138137
)
139138
ordering = (

netbox_acls/tests/models/test_aclinterfaceassignments.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,40 @@ def test_acl_already_assigned_fail(self):
138138
Test that ACLInterfaceAssignment fails validation
139139
if the interface already has an ACL assigned in the same direction.
140140
"""
141-
pass
142-
# TODO: test_acl_already_assigned_fail - VM & Device
141+
# Access List 1
142+
device_acl1 = AccessList(
143+
name="STANDARD_ACL1",
144+
assigned_object=self.device1,
145+
type="standard",
146+
default_action="permit",
147+
comments="STANDARD_ACL",
148+
)
149+
device_acl1.save()
150+
acl_device_interface1 = ACLInterfaceAssignment(
151+
access_list=device_acl1,
152+
direction="ingress",
153+
assigned_object=self.device_interface1,
154+
)
155+
acl_device_interface1.full_clean()
156+
acl_device_interface1.save()
157+
158+
# Access List 2
159+
device_acl2 = AccessList(
160+
name="STANDARD_ACL2",
161+
assigned_object=self.device1,
162+
type="standard",
163+
default_action="permit",
164+
comments="STANDARD_ACL",
165+
)
166+
device_acl2.save()
167+
acl_device_interface2 = ACLInterfaceAssignment(
168+
access_list=device_acl2,
169+
direction="ingress",
170+
assigned_object=self.device_interface1,
171+
)
172+
with self.assertRaises(ValidationError):
173+
acl_device_interface2.full_clean()
174+
acl_device_interface2.save()
143175

144176
def test_valid_acl_interface_assignment_choices(self):
145177
"""

0 commit comments

Comments
 (0)