diff --git a/.devcontainer/docker-compose.override.yml b/.devcontainer/docker-compose.override.yml index f8d64d2..31816f6 100644 --- a/.devcontainer/docker-compose.override.yml +++ b/.devcontainer/docker-compose.override.yml @@ -5,7 +5,7 @@ services: dockerfile: Dockerfile-plugin_dev context: . ports: - - 8000:8080 + - "8000:8080" volumes: - ../:/opt/netbox/netbox/netbox-acls - ~/.gitconfig:/home/vscode/.gitconfig:z,ro diff --git a/.devcontainer/initializers/cables.yml b/.devcontainer/initializers/cables.yml index b4a9137..9d100a5 100644 --- a/.devcontainer/initializers/cables.yml +++ b/.devcontainer/initializers/cables.yml @@ -3,7 +3,7 @@ # ``` # termination_x_name -> name of interface # termination_x_device -> name of the device interface belongs to -# termination_x_class -> required if different than 'Interface' which is the default +# termination_x_class -> required if different from 'Interface' which is the default # ``` # # Supported termination classes: Interface, ConsolePort, ConsoleServerPort, FrontPort, RearPort, PowerPort, PowerOutlet diff --git a/.gitignore b/.gitignore index 7059dd0..841c0a2 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,5 @@ cython_debug/ # VS Code .vscode/ +# JetBrains +.idea/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9cf3305..cacec27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,9 +3,9 @@ ## Reporting Bugs * First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) -of NetBox or this plugin [latest stable version](https://github.com/ryanmerolle/netbox-acls/releases). +of NetBox or this plugin is at [latest stable version](https://github.com/ryanmerolle/netbox-acls/releases). If you're running an older version, it's possible that the bug has already been fixed -or you are running a version of the plugin not tested with the NetBox version +or, you are running a version of the plugin not tested with the NetBox version you are running [Compatibility Matrix](./README.md#compatibility). * Next, check the GitHub [issues list](https://github.com/ryanmerolle/netbox-acls/issues) diff --git a/README.md b/README.md index 7668aab..cd51550 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Based on the NetBox plugin tutorial by [jeremystretch](https://github.com/jeremy - [demo repository](https://github.com/netbox-community/netbox-plugin-demo) - [tutorial](https://github.com/netbox-community/netbox-plugin-tutorial) -All credit should go to Jeremy. Thanks Jeremy! +All credit should go to Jeremy. Thanks, Jeremy! This project just looks to build on top of this framework and model presented. @@ -69,12 +69,12 @@ PLUGINS_CONFIG = { To develop this plugin further one can use the included .devcontainer configuration. This configuration creates a docker container which includes a fully working netbox installation. Currently it should work when using WSL 2. For this to work make sure you have Docker Desktop installed and the WSL 2 integrations activated. 1. In the WSL terminal, enter `code` to run Visual studio code. -1. Install the devcontainer extension "ms-vscode-remote.remote-containers" -1. Press Ctrl+Shift+P and use the "Dev Container: Clone Repository in Container Volume" function to clone this repository. This will take a while depending on your computer -1. If you'd like the netbox instance to be prepopulated run `make Makefile example_initializers` and `make Makefile load_initializers` -1. Start the netbox instance using `make Makefile all` +2. Install the devcontainer extension "ms-vscode-remote.remote-containers" +3. Press Ctrl+Shift+P and use the "Dev Container: Clone Repository in Container Volume" function to clone this repository. This will take a while depending on your computer +4. If you'd like the netbox instance to be prepopulated run `make Makefile example_initializers` and `make Makefile load_initializers` +5. Start the netbox instance using `make Makefile all` -Your netbox instance will be served under 0.0.0.0:8000 so it should now be available under localhost:8000. +Your netbox instance will be served under 0.0.0.0:8000, so it should now be available under localhost:8000. ## Screenshots diff --git a/netbox_acls/api/serializers.py b/netbox_acls/api/serializers.py index e5dd7a2..b84a0bd 100644 --- a/netbox_acls/api/serializers.py +++ b/netbox_acls/api/serializers.py @@ -9,6 +9,7 @@ from drf_yasg.utils import swagger_serializer_method from ipam.api.serializers import NestedPrefixSerializer from netbox.api.fields import ContentTypeField from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX from rest_framework import serializers from utilities.api import get_serializer_for_model @@ -82,7 +83,10 @@ class AccessListSerializer(NetBoxModelSerializer): @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_assigned_object(self, obj): - serializer = get_serializer_for_model(obj.assigned_object, prefix="Nested") + serializer = get_serializer_for_model( + obj.assigned_object, + prefix=NESTED_SERIALIZER_PREFIX, + ) context = {"request": self.context["request"]} return serializer(obj.assigned_object, context=context).data @@ -96,6 +100,7 @@ class AccessListSerializer(NetBoxModelSerializer): # Check that the GFK object is valid. if "assigned_object_type" in data and "assigned_object_id" in data: + # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed. try: assigned_object = data[ # noqa: F841 "assigned_object_type" @@ -161,21 +166,25 @@ class ACLInterfaceAssignmentSerializer(NetBoxModelSerializer): @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_assigned_object(self, obj): - serializer = get_serializer_for_model(obj.assigned_object, prefix="Nested") + serializer = get_serializer_for_model( + obj.assigned_object, + prefix=NESTED_SERIALIZER_PREFIX, + ) context = {"request": self.context["request"]} return serializer(obj.assigned_object, context=context).data def validate(self, data): """ - Validate the AccessList django model model's inputs before allowing it to update the instance. + 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 - # Check that the GFK object is vlaid. + # Check that the GFK object is valid. if "assigned_object_type" in data and "assigned_object_id" in data: + # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed. try: assigned_object = data[ # noqa: F841 "assigned_object_type" @@ -200,6 +209,8 @@ class ACLInterfaceAssignmentSerializer(NetBoxModelSerializer): .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 = ( @@ -253,7 +264,7 @@ class ACLStandardRuleSerializer(NetBoxModelSerializer): def validate(self, data): """ - Validate the ACLStandardRule django model model's inputs before allowing it to update the instance: + 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. """ @@ -322,7 +333,7 @@ class ACLExtendedRuleSerializer(NetBoxModelSerializer): def validate(self, data): """ - Validate the ACLExtendedRule django model model's inputs before allowing it to update the instance: + 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_ports set. diff --git a/netbox_acls/choices.py b/netbox_acls/choices.py index 8247d45..4b1c5b2 100644 --- a/netbox_acls/choices.py +++ b/netbox_acls/choices.py @@ -51,9 +51,12 @@ class ACLAssignmentDirectionChoices(ChoiceSet): Defines the direction of the application of the ACL on an associated interface. """ + DIRECTION_INGRESS = "ingress" + DIRECTION_EGRESS = "egress" + CHOICES = [ - ("ingress", "Ingress", "blue"), - ("egress", "Egress", "purple"), + (DIRECTION_INGRESS, "Ingress", "blue"), + (DIRECTION_EGRESS, "Egress", "purple"), ] @@ -62,9 +65,12 @@ class ACLTypeChoices(ChoiceSet): Defines the choices availble for the Access Lists plugin specific to ACL type. """ + TYPE_STANDARD = "standard" + TYPE_EXTENDED = "extended" + CHOICES = [ - ("extended", "Extended", "purple"), - ("standard", "Standard", "blue"), + (TYPE_EXTENDED, "Extended", "purple"), + (TYPE_STANDARD, "Standard", "blue"), ] @@ -73,8 +79,12 @@ class ACLProtocolChoices(ChoiceSet): Defines the choices availble for the Access Lists plugin specific to ACL Rule protocol. """ + PROTOCOL_ICMP = "icmp" + PROTOCOL_TCP = "tcp" + PROTOCOL_UDP = "udp" + CHOICES = [ - ("icmp", "ICMP", "purple"), - ("tcp", "TCP", "blue"), - ("udp", "UDP", "orange"), + (PROTOCOL_ICMP, "ICMP", "purple"), + (PROTOCOL_TCP, "TCP", "blue"), + (PROTOCOL_UDP, "UDP", "orange"), ] diff --git a/netbox_acls/forms/models.py b/netbox_acls/forms/models.py index 38f7c0c..c02640f 100644 --- a/netbox_acls/forms/models.py +++ b/netbox_acls/forms/models.py @@ -22,6 +22,7 @@ from virtualization.models import ( VMInterface, ) +from ..choices import ACLTypeChoices from ..models import ( AccessList, ACLExtendedRule, @@ -236,8 +237,12 @@ class AccessListForm(NetBoxModelForm): "name": [error_same_acl_name], } # Check if Access List has no existing rules before change the Access List's type. - if (acl_type == "extended" and self.instance.aclstandardrules.exists()) or ( - acl_type == "standard" and self.instance.aclextendedrules.exists() + if ( + acl_type == ACLTypeChoices.TYPE_EXTENDED + and self.instance.aclstandardrules.exists() + ) or ( + acl_type == ACLTypeChoices.TYPE_STANDARD + and self.instance.aclextendedrules.exists() ): error_message["type"] = [ "This ACL has ACL rules associated, CANNOT change ACL type.", @@ -312,10 +317,6 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm): ), ) comments = CommentField() - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False, - ) def __init__(self, *args, **kwargs): @@ -355,7 +356,7 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm): """ Validates form inputs before submitting: - Check if both interface and vminterface are set. - - Check if neither interface or vminterface are set. + - Check if neither interface nor vminterface are set. - Check that an interface's parent device/virtual_machine is assigned to the Access List. - Check that an interface's parent device/virtual_machine is assigned to the Access List. - Check for duplicate entry. (Because of GFK) @@ -367,7 +368,6 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm): direction = cleaned_data.get("direction") interface = cleaned_data.get("interface") vminterface = cleaned_data.get("vminterface") - assigned_object = cleaned_data.get("assigned_object") # Check if both interface and vminterface are set. if interface and vminterface: @@ -403,40 +403,42 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm): ).pk access_list_host = AccessList.objects.get(pk=access_list.pk).assigned_object - # Check that an interface's parent device/virtual_machine is assigned to the Access List. - if access_list_host != host: - error_acl_not_assigned_to_host = "Access List not present on selected host." - error_message |= { - "access_list": [error_acl_not_assigned_to_host], - assigned_object_type: [error_acl_not_assigned_to_host], - host_type: [error_acl_not_assigned_to_host], - } - # Check for duplicate entry. - if ACLInterfaceAssignment.objects.filter( - access_list=access_list, - assigned_object_id=assigned_object_id, - assigned_object_type=assigned_object_type_id, - direction=direction, - ).exists(): - error_duplicate_entry = "An ACL with this name is already associated to this interface & direction." - error_message |= { - "access_list": [error_duplicate_entry], - "direction": [error_duplicate_entry], - assigned_object_type: [error_duplicate_entry], - } - # Check that the interface does not have an existing ACL applied in the direction already. - if ACLInterfaceAssignment.objects.filter( - assigned_object_id=assigned_object_id, - assigned_object_type=assigned_object_type_id, - direction=direction, - ).exists(): - error_interface_already_assigned = ( - "Interfaces can only have 1 Access List assigned in each direction." - ) - error_message |= { - "direction": [error_interface_already_assigned], - assigned_object_type: [error_interface_already_assigned], - } + # Check that an interface's parent device/virtual_machine is assigned to the Access List. + if access_list_host != host: + error_acl_not_assigned_to_host = ( + "Access List not present on selected host." + ) + error_message |= { + "access_list": [error_acl_not_assigned_to_host], + assigned_object_type: [error_acl_not_assigned_to_host], + host_type: [error_acl_not_assigned_to_host], + } + # Check for duplicate entry. + if ACLInterfaceAssignment.objects.filter( + access_list=access_list, + assigned_object_id=assigned_object_id, + assigned_object_type=assigned_object_type_id, + direction=direction, + ).exists(): + error_duplicate_entry = "An ACL with this name is already associated to this interface & direction." + error_message |= { + "access_list": [error_duplicate_entry], + "direction": [error_duplicate_entry], + assigned_object_type: [error_duplicate_entry], + } + # Check that the interface does not have an existing ACL applied in the direction already. + if ACLInterfaceAssignment.objects.filter( + assigned_object_id=assigned_object_id, + assigned_object_type=assigned_object_type_id, + direction=direction, + ).exists(): + error_interface_already_assigned = ( + "Interfaces can only have 1 Access List assigned in each direction." + ) + error_message |= { + "direction": [error_interface_already_assigned], + assigned_object_type: [error_interface_already_assigned], + } if error_message: raise forms.ValidationError(error_message) @@ -460,7 +462,7 @@ class ACLStandardRuleForm(NetBoxModelForm): access_list = DynamicModelChoiceField( queryset=AccessList.objects.all(), query_params={ - "type": "standard", + "type": ACLTypeChoices.TYPE_STANDARD, }, help_text=mark_safe( "*Note: This field will only display Standard ACLs.", @@ -473,10 +475,6 @@ class ACLStandardRuleForm(NetBoxModelForm): help_text=help_text_acl_rule_logic, label="Source Prefix", ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False, - ) fieldsets = ( ("Access List Details", ("access_list", "description", "tags")), @@ -542,17 +540,14 @@ class ACLExtendedRuleForm(NetBoxModelForm): access_list = DynamicModelChoiceField( queryset=AccessList.objects.all(), query_params={ - "type": "extended", + "type": ACLTypeChoices.TYPE_EXTENDED, }, help_text=mark_safe( "*Note: This field will only display Extended ACLs.", ), label="Access List", ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False, - ) + source_prefix = DynamicModelChoiceField( queryset=Prefix.objects.all(), required=False, diff --git a/netbox_acls/models/access_list_rules.py b/netbox_acls/models/access_list_rules.py index f37aca8..5656720 100644 --- a/netbox_acls/models/access_list_rules.py +++ b/netbox_acls/models/access_list_rules.py @@ -8,7 +8,7 @@ from django.db import models from django.urls import reverse from netbox.models import NetBoxModel -from ..choices import ACLProtocolChoices, ACLRuleActionChoices +from ..choices import ACLProtocolChoices, ACLRuleActionChoices, ACLTypeChoices from .access_lists import AccessList __all__ = ( @@ -86,7 +86,7 @@ class ACLStandardRule(ACLRule): on_delete=models.CASCADE, to=AccessList, verbose_name="Standard Access List", - limit_choices_to={"type": "standard"}, + limit_choices_to={"type": ACLTypeChoices.TYPE_STANDARD}, related_name="aclstandardrules", ) diff --git a/netbox_acls/template_content.py b/netbox_acls/template_content.py index 0de1844..598e71d 100644 --- a/netbox_acls/template_content.py +++ b/netbox_acls/template_content.py @@ -31,6 +31,9 @@ class ACLInterfaceAssignments(PluginTemplateExtension): elif ctype.model == "vminterface": parent_type = "virtual_machine" parent_id = obj.virtual_machine.pk + else: + parent_type = None + parent_id = None return self.render( "inc/assigned_interface/access_lists.html", diff --git a/netbox_acls/views.py b/netbox_acls/views.py index df1eae8..19a0204 100644 --- a/netbox_acls/views.py +++ b/netbox_acls/views.py @@ -6,7 +6,7 @@ Specifically, all the various interactions with a client. from django.db.models import Count from netbox.views import generic -from . import filtersets, forms, models, tables +from . import choices, filtersets, forms, models, tables __all__ = ( "AccessListView", @@ -48,15 +48,22 @@ class AccessListView(generic.ObjectView): """ Depending on the Access List type, the list view will return the required ACL Rule using the previous defined tables in tables.py. """ - if instance.type == "extended": - table = tables.ACLExtendedRuleTable(instance.aclextendedrules.all()) - elif instance.type == "standard": - table = tables.ACLStandardRuleTable(instance.aclstandardrules.all()) - table.configure(request) - return { - "rules_table": table, - } + if instance.type == choices.ACLTypeChoices.TYPE_EXTENDED: + table = tables.ACLExtendedRuleTable(instance.aclextendedrules.all()) + elif instance.type == choices.ACLTypeChoices.TYPE_STANDARD: + table = tables.ACLStandardRuleTable(instance.aclstandardrules.all()) + else: + table = None + + if table: + table.columns.hide("access_list") + table.configure(request) + + return { + "rules_table": table, + } + return {} class AccessListListView(generic.ObjectListView): @@ -84,7 +91,7 @@ class AccessListEditView(generic.ObjectEditView): class AccessListDeleteView(generic.ObjectDeleteView): """ - Defines the delete view for the AccessLists django model. + Defines delete view for the AccessLists django model. """ queryset = models.AccessList.objects.all() @@ -129,10 +136,20 @@ class ACLInterfaceAssignmentEditView(generic.ObjectEditView): form = forms.ACLInterfaceAssignmentForm template_name = "netbox_acls/aclinterfaceassignment_edit.html" + def get_extra_addanother_params(self, request): + """ + Returns a dictionary of additional parameters to be passed to the "Add Another" button. + """ + + return { + "access_list": request.GET.get("access_list") or request.POST.get("access_list"), + "direction": request.GET.get("direction") or request.POST.get("direction"), + } + class ACLInterfaceAssignmentDeleteView(generic.ObjectDeleteView): """ - Defines the delete view for the ACLInterfaceAssignments django model. + Defines delete view for the ACLInterfaceAssignments django model. """ queryset = models.ACLInterfaceAssignment.objects.all() @@ -176,10 +193,19 @@ class ACLStandardRuleEditView(generic.ObjectEditView): queryset = models.ACLStandardRule.objects.all() form = forms.ACLStandardRuleForm + def get_extra_addanother_params(self, request): + """ + Returns a dictionary of additional parameters to be passed to the "Add Another" button. + """ + + return { + "access_list": request.GET.get("access_list") or request.POST.get("access_list"), + } + class ACLStandardRuleDeleteView(generic.ObjectDeleteView): """ - Defines the delete view for the ACLStandardRules django model. + Defines delete view for the ACLStandardRules django model. """ queryset = models.ACLStandardRule.objects.all() @@ -223,10 +249,19 @@ class ACLExtendedRuleEditView(generic.ObjectEditView): queryset = models.ACLExtendedRule.objects.all() form = forms.ACLExtendedRuleForm + def get_extra_addanother_params(self, request): + """ + Returns a dictionary of additional parameters to be passed to the "Add Another" button. + """ + + return { + "access_list": request.GET.get("access_list") or request.POST.get("access_list"), + } + class ACLExtendedRuleDeleteView(generic.ObjectDeleteView): """ - Defines the delete view for the ACLExtendedRules django model. + Defines delete view for the ACLExtendedRules django model. """ queryset = models.ACLExtendedRule.objects.all()