Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
50ad58f
feat(models): Add generic source/destination fields for ACL rules
pheus Aug 7, 2025
a92f2e9
feat(migrations): Update ACL rule migrations for source/destination
pheus Aug 7, 2025
d877eb4
feat(migrations): Add prefix assignment migration for ACL rules
pheus Aug 8, 2025
7387f20
feat(tests): Add generic object tests for ACL rules models
pheus Aug 9, 2025
f2a1dc8
feat(tables): Add generic src and dst columns for ACL rules
pheus Aug 9, 2025
3fcafb8
feat(filtersets): Extend filter support for ACL source/destination
pheus Aug 10, 2025
6924365
feat(forms): Add generic source and destination fields for ACL rules
pheus Aug 10, 2025
0df9b01
feat(templates): Update source/destination field labels
pheus Aug 10, 2025
2f1c747
feat(views): Update prefetch fields for ACL rules
pheus Aug 10, 2025
52a8f13
feat(serializers): Add generic source/destination support
pheus Aug 13, 2025
9790043
feat(views): Improve view docstrings and adjust prefetch fields
pheus Aug 13, 2025
dcf96d0
feat(graphql): Add generic source/destination field support
pheus Aug 14, 2025
6647390
feat(graphql): Add ContentType filters for ACL rules
pheus Aug 14, 2025
2b2f628
feat(tests): Update ACL rule tests for generic fields
pheus Aug 15, 2025
3732819
fix(choices): Correct typos in docstrings for ACL choices
pheus Aug 18, 2025
5b8390a
fix(filtersets): Update filter fields for ACL source/destination
pheus Aug 23, 2025
1fc9b60
feat(filtersets): Add remark and port filters for ACL rules
pheus Aug 23, 2025
dc69807
fix(migrations): Ensure database alias is used in ACL rule updates
pheus Aug 30, 2025
35be1df
Merge branch '129-add-source-and-destination-assigned-object-support'…
pheus Sep 26, 2025
4a86457
refactor(models): Unify ACL assignment and update validation
pheus Aug 23, 2025
ea323af
refactor(migrations): Rename and update ACL assignments model
pheus Sep 26, 2025
150570b
refactor(tests): Remove legacy assigned object tests for AccessList
pheus Aug 24, 2025
08a140b
refactor(tables): Update tables to align with ACLAssignment model
pheus Aug 24, 2025
e343fcd
refactor(filtersets): Update to use ACLAssignment model
pheus Aug 24, 2025
d861b97
refactor(forms): Migrate forms to use ACLAssignment model
pheus Aug 24, 2025
1a2eff5
refactor(templates): Update templates to use ACLAssignment model
pheus Aug 25, 2025
5c00a57
refactor(views): Migrate views to ACLAssignment model
pheus Aug 26, 2025
31e64ab
refactor(urls): Update routes to align with ACLAssignment model
pheus Aug 26, 2025
400df2b
refactor(serializers): Update serializers to use ACLAssignment model
pheus Aug 27, 2025
9427e27
refactor(views): Replace ACLInterfaceAssignment with ACLAssignment
pheus Aug 27, 2025
8c1f07f
refactor(api): Update URLs to use ACLAssignment routes
pheus Aug 27, 2025
d7ab05f
refactor(graphql): Replace ACLInterfaceAssignment with ACLAssignment
pheus Aug 28, 2025
0ffbf3f
refactor(graphql): Replace ACLInterfaceAssignment with ACLAssignment
pheus Aug 28, 2025
681b57b
refactor(graphql): Replace ACLInterfaceAssignment with ACLAssignment
pheus Aug 28, 2025
89f66a1
refactor(tests): Update test cases to use ACLAssignment model
pheus Sep 26, 2025
186093d
refactor(navigation): Replace ACLInterfaceAssignment with ACLAssignment
pheus Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 96 additions & 80 deletions netbox_acls/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,30 @@

from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from ipam.api.serializers import PrefixSerializer
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
from rest_framework import serializers
from utilities.api import get_serializer_for_model

from ..constants import ACL_HOST_ASSIGNMENT_MODELS, ACL_INTERFACE_ASSIGNMENT_MODELS
from ..constants import ACL_ASSIGNMENT_MODELS, ACL_RULE_SOURCE_DESTINATION_MODELS
from ..models import (
AccessList,
ACLExtendedRule,
ACLInterfaceAssignment,
ACLAssignment,
ACLStandardRule,
)

__all__ = [
"AccessListSerializer",
"ACLInterfaceAssignmentSerializer",
"ACLAssignmentSerializer",
"ACLStandardRuleSerializer",
"ACLExtendedRuleSerializer",
]

# Sets a standard error message for ACL rules with an action of remark, but no remark set.
error_message_no_remark = "Action is set to remark, you MUST add a remark."
# Sets a standard error message for ACL rules with an action of remark, but no source_prefix is set.
error_message_action_remark_source_prefix_set = "Action is set to remark, Source Prefix CANNOT be set."
# Sets a standard error message for ACL rules with an action of remark, but no source is set.
error_message_action_remark_source_set = "Action is set to remark, Source CANNOT be set."
# Sets a standard error message for ACL rules with an action not set to remark, but no remark is set.
error_message_remark_without_action_remark = "CANNOT set remark unless action is set to remark."
# Sets a standard error message for ACL rules no associated with an ACL of the same type.
Expand All @@ -45,10 +44,6 @@ class AccessListSerializer(NetBoxModelSerializer):
view_name="plugins-api:netbox_acls-api:accesslist-detail",
)
rule_count = serializers.IntegerField(read_only=True)
assigned_object_type = ContentTypeField(
queryset=ContentType.objects.filter(ACL_HOST_ASSIGNMENT_MODELS),
)
assigned_object = serializers.SerializerMethodField(read_only=True)

class Meta:
"""
Expand All @@ -61,9 +56,6 @@ class Meta:
"url",
"display",
"name",
"assigned_object_type",
"assigned_object_id",
"assigned_object",
"type",
"default_action",
"comments",
Expand All @@ -75,14 +67,6 @@ class Meta:
)
brief_fields = ("id", "url", "display", "name")

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_assigned_object(self, obj):
if obj.assigned_object is None:
return None
serializer = get_serializer_for_model(obj.assigned_object)
context = {"request": self.context["request"]}
return serializer(obj.assigned_object, nested=True, context=context).data

def validate(self, data):
"""
Validates api inputs before processing:
Expand All @@ -103,26 +87,26 @@ def validate(self, data):
return super().validate(data)


class ACLInterfaceAssignmentSerializer(NetBoxModelSerializer):
class ACLAssignmentSerializer(NetBoxModelSerializer):
"""
Defines the serializer for the django ACLInterfaceAssignment model and associates it with a view.
Defines the serializer for the django ACLAssignment model and associates it with a view.
"""

url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_acls-api:aclinterfaceassignment-detail",
view_name="plugins-api:netbox_acls-api:aclassignment-detail",
)
access_list = AccessListSerializer(nested=True, required=True)
assigned_object_type = ContentTypeField(
queryset=ContentType.objects.filter(ACL_INTERFACE_ASSIGNMENT_MODELS),
queryset=ContentType.objects.filter(ACL_ASSIGNMENT_MODELS),
)
assigned_object = serializers.SerializerMethodField(read_only=True)

class Meta:
"""
Associates the django model ACLInterfaceAssignment & fields to the serializer.
Associates the django model ACLAssignment & fields to the serializer.
"""

model = ACLInterfaceAssignment
model = ACLAssignment
fields = (
"id",
"url",
Expand All @@ -148,34 +132,6 @@ def get_assigned_object(self, obj):
context = {"request": self.context["request"]}
return serializer(obj.assigned_object, nested=True, context=context).data

def validate(self, data):
"""
Validate the AccessList django model's inputs before allowing it to update the instance.
- Check that the GFK object is valid.
- Check that the associated interface's parent host has the selected ACL defined.
"""
error_message = {}
acl_host = data["access_list"].assigned_object

if data["assigned_object_type"].model == "interface":
interface_host = data["assigned_object_type"].get_object_for_this_type(id=data["assigned_object_id"]).device
elif data["assigned_object_type"].model == "vminterface":
interface_host = (
data["assigned_object_type"].get_object_for_this_type(id=data["assigned_object_id"]).virtual_machine
)
else:
interface_host = None
# Check that the associated interface's parent host has the selected ACL defined.
if acl_host != interface_host:
error_acl_not_assigned_to_host = "Access List not present on the selected interface's host."
error_message["access_list"] = [error_acl_not_assigned_to_host]
error_message["assigned_object_id"] = [error_acl_not_assigned_to_host]

if error_message:
raise serializers.ValidationError(error_message)

return super().validate(data)


class ACLStandardRuleSerializer(NetBoxModelSerializer):
"""
Expand All @@ -186,12 +142,18 @@ class ACLStandardRuleSerializer(NetBoxModelSerializer):
view_name="plugins-api:netbox_acls-api:aclstandardrule-detail",
)
access_list = AccessListSerializer(nested=True, required=True)
source_prefix = PrefixSerializer(
nested=True,
source_type = ContentTypeField(
queryset=ContentType.objects.filter(ACL_RULE_SOURCE_DESTINATION_MODELS),
required=False,
default=None,
allow_null=True,
)
source_id = serializers.IntegerField(
required=False,
default=None,
allow_null=True,
)
source = serializers.SerializerMethodField(read_only=True)

class Meta:
"""
Expand All @@ -207,20 +169,36 @@ class Meta:
"index",
"action",
"remark",
"source_prefix",
"source_type",
"source_id",
"source",
"description",
"tags",
"created",
"custom_fields",
"last_updated",
)
brief_fields = ("id", "url", "display", "access_list", "index")
brief_fields = (
"id",
"url",
"display",
"access_list",
"index",
)

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_source(self, obj):
if obj.source_id is None:
return None
serializer = get_serializer_for_model(obj.source)
context = {"request": self.context["request"]}
return serializer(obj.source, nested=True, context=context).data

def validate(self, data):
"""
Validate the ACLStandardRule django model's inputs before allowing it to update the instance:
- Check if action set to remark, but no remark set.
- Check if action set to remark, but source_prefix set.
- Check if action set to remark, but source set.
"""
error_message = {}

Expand All @@ -230,10 +208,10 @@ def validate(self, data):
error_message["remark"] = [
error_message_no_remark,
]
# Check if action set to remark, but source_prefix set.
if data.get("source_prefix"):
error_message["source_prefix"] = [
error_message_action_remark_source_prefix_set,
# Check if action set to remark, but the source set.
if data.get("source"):
error_message["source"] = [
error_message_action_remark_source_set,
]

if error_message:
Expand All @@ -251,18 +229,30 @@ class ACLExtendedRuleSerializer(NetBoxModelSerializer):
view_name="plugins-api:netbox_acls-api:aclextendedrule-detail",
)
access_list = AccessListSerializer(nested=True, required=True)
source_prefix = PrefixSerializer(
nested=True,
source_type = ContentTypeField(
queryset=ContentType.objects.filter(ACL_RULE_SOURCE_DESTINATION_MODELS),
required=False,
default=None,
allow_null=True,
)
source_id = serializers.IntegerField(
required=False,
default=None,
allow_null=True,
)
destination_prefix = PrefixSerializer(
nested=True,
source = serializers.SerializerMethodField(read_only=True)
destination_type = ContentTypeField(
queryset=ContentType.objects.filter(ACL_RULE_SOURCE_DESTINATION_MODELS),
required=False,
default=None,
allow_null=True,
)
destination_id = serializers.IntegerField(
required=False,
default=None,
allow_null=True,
)
destination = serializers.SerializerMethodField(read_only=True)

class Meta:
"""
Expand All @@ -279,25 +269,51 @@ class Meta:
"action",
"remark",
"protocol",
"source_prefix",
"source_type",
"source_id",
"source",
"source_ports",
"destination_prefix",
"destination_type",
"destination_id",
"destination",
"destination_ports",
"description",
"tags",
"created",
"custom_fields",
"last_updated",
)
brief_fields = ("id", "url", "display", "access_list", "index")
brief_fields = (
"id",
"url",
"display",
"access_list",
"index",
)

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_source(self, obj):
if obj.source_id is None:
return None
serializer = get_serializer_for_model(obj.source)
context = {"request": self.context["request"]}
return serializer(obj.source, nested=True, context=context).data

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_destination(self, obj):
if obj.destination_id is None:
return None
serializer = get_serializer_for_model(obj.destination)
context = {"request": self.context["request"]}
return serializer(obj.destination, nested=True, context=context).data

def validate(self, data):
"""
Validate the ACLExtendedRule django model's inputs before allowing it to update the instance:
- Check if action set to remark, but no remark set.
- Check if action set to remark, but source_prefix set.
- Check if action set to remark, but source set.
- Check if action set to remark, but source_ports set.
- Check if action set to remark, but destination_prefix set.
- Check if action set to remark, but destination set.
- Check if action set to remark, but destination_ports set.
- Check if action set to remark, but protocol set.
- Check if action set to remark, but protocol set.
Expand All @@ -310,19 +326,19 @@ def validate(self, data):
error_message["remark"] = [
error_message_no_remark,
]
# Check if action set to remark, but source_prefix set.
if data.get("source_prefix"):
error_message["source_prefix"] = [
error_message_action_remark_source_prefix_set,
# Check if action set to remark, but the source set.
if data.get("source"):
error_message["source"] = [
error_message_action_remark_source_set,
]
# Check if action set to remark, but source_ports set.
if data.get("source_ports"):
error_message["source_ports"] = [
"Action is set to remark, Source Ports CANNOT be set.",
]
# Check if action set to remark, but destination_prefix set.
if data.get("destination_prefix"):
error_message["destination_prefix"] = [
# Check if action set to remark, but destination set.
if data.get("destination"):
error_message["destination"] = [
"Action is set to remark, Destination Prefix CANNOT be set.",
]
# Check if action set to remark, but destination_ports set.
Expand Down
2 changes: 1 addition & 1 deletion netbox_acls/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

router = NetBoxRouter()
router.register("access-lists", views.AccessListViewSet)
router.register("interface-assignments", views.ACLInterfaceAssignmentViewSet)
router.register("assignments", views.ACLAssignmentViewSet)
router.register("standard-acl-rules", views.ACLStandardRuleViewSet)
router.register("extended-acl-rules", views.ACLExtendedRuleViewSet)

Expand Down
Loading