Merge pull request #40 from ryanmerolle/2_ACLRule_models

Closes [Feature]: Add more advanced ACLRule logic #29
Closes [Bug]: Add comment section title to ACL add page #27
Closes [Bug]: Filtering multiple choices not working #15
Closes [Feature]: Limit ACL Rules input options based on ACL type #2
This commit is contained in:
Ryan Merolle 2022-07-20 15:58:58 -04:00 committed by GitHub
commit 00c5368bb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1437 additions and 350 deletions

View File

@ -27,30 +27,40 @@
//"python.sortImports.args": [
// "--profile=black"
//],
//"python.sortImports.path": "/opt/netbox/venv/bin/isort",
"python.sortImports.path": "/opt/netbox/venv/bin/isort",
"python.analysis.typeCheckingMode": "strict",
"python.analysis.extraPaths": [
"/opt/netbox/",
"/opt/netbox/netbox"
],
"python.autoComplete.extraPaths": [
"/opt/netbox/",
"/opt/netbox/netbox"
"/opt/netbox/netbox/",
"/opt/netbox/netbox/**",
"/opt/netbox/netbox/**/**"
],
"python.defaultInterpreterPath": "/opt/netbox/venv/bin/python",
"python.formatting.autopep8Path": "/opt/netbox/venv/lib/python3.9/site-packages/autopep8",
"python.formatting.blackPath": "/opt/netbox/venv/lib/python3.9/site-packages/black",
"python.defaultInterpreterPath": "/opt/netbox/venv/bin/python3",
"python.formatting.autopep8Path": "/opt/netbox/venv/bin/autopep8",
"python.formatting.blackPath": "/opt/netbox/venv/bin/black",
"python.formatting.provider": "black",
"python.formatting.yapfPath": "/opt/netbox/venv/lib/python3.9/site-packages/yapf",
"python.linting.banditPath": "/opt/netbox/venv/lib/python3.9/site-packages/bandit",
"python.formatting.yapfPath": "/opt/netbox/venv/bin/yapf",
"python.linting.banditPath": "/opt/netbox/venv/bin/bandit",
"python.linting.enabled": true,
"python.linting.flake8Path": "/opt/netbox/venv/lib/python3.9/site-packages/flake8",
"python.linting.mypyPath": "/opt/netbox/venv/lib/python3.9/site-packages/mypy",
"python.linting.pycodestylePath": "/opt/netbox/venv/lib/python3.9/site-packages/pycodestyle",
"python.linting.pydocstylePath": "/opt/netbox/venv/lib/python3.9/site-packages/pydocstyle",
"python.linting.flake8Path": "/opt/netbox/venv/bin/flake8",
"python.linting.mypyPath": "//opt/netbox/venv/bin/mypy",
"python.linting.pycodestylePath": "/opt/netbox/venv/bin/pycodestyle",
"python.linting.pydocstylePath": "/opt/netbox/venv/bin/pydocstyle",
"python.linting.pylintArgs": [
"--load-plugins",
"pylint_django",
"--errors-only",
"--load-plugins=pylint_django",
"--django-settings-module=/opt/netbox/netbox/netbox/netbox.settings",
"--enable=W0602,W0611,W0612,W0613,W0614"
],
"python.linting.pylintEnabled": true,
"python.linting.pylintPath": "/opt/netbox/venv/lib/python3.9/site-packages/pylint",
"python.pythonPath": "/opt/netbox/venv/bin/python",
"python.linting.pylintPath": "/opt/netbox/venv/bin/pylint",
"python.pythonPath": "/opt/netbox/venv/bin/python3",
"python.terminal.activateEnvironment": true,
"python.venvPath": "/opt/netbox/"
},
@ -65,7 +75,11 @@
"aaron-bond.better-comments",
"GitHub.codespaces",
"codezombiech.gitignore",
"Tyriar.sort-lines"
"Tyriar.sort-lines",
"GitHub.vscode-pull-request-github",
"sourcery.sourcery",
"mintlify.document",
"batisteo.vscode-django"
]
}
},

View File

@ -27,3 +27,11 @@
status: active
tenant: tenant2
vlan: vlan2
- description: prefix3
prefix: 192.168.1.0/24
site: AMS 1
status: active
- description: prefix4
prefix: 192.168.11.0/24
site: AMS 2
status: active

View File

@ -8,4 +8,5 @@ pre-commit
pycodestyle
pydocstyle
pylint
pylint-django
yapf

View File

@ -1,6 +1,6 @@
---
name: 📖 Documentation Change
description: Suggest an addition or modification to the NetBox access-list plugin documentation
description: Suggest an addition or modification to the NetBox Access Lists plugin documentation
title: "[Docs]: "
labels: ["documentation"]
body:

View File

@ -1,6 +1,6 @@
# netbox-access-lists
A NetBox plugin for Access-List management
A NetBox plugin for Access List management
## Origin

View File

@ -1,12 +1,15 @@
"""
Define the NetBox Plugin
"""
from extras.plugins import PluginConfig
class NetBoxAccessListsConfig(PluginConfig):
name = 'netbox_access_lists'
verbose_name = ' NetBox Access Lists'
verbose_name = 'Access Lists'
description = 'Manage simple ACLs in NetBox'
version = '0.1'
base_url = 'access-lists'
config = NetBoxAccessListsConfig

View File

@ -1,40 +1,77 @@
"""
Serializers control the translation of client data to and from Python objects,
while Django itself handles the database abstraction.
"""
from dcim.api.nested_serializers import NestedDeviceSerializer
from ipam.api.serializers import NestedPrefixSerializer
from netbox.api.serializers import (NetBoxModelSerializer,
WritableNestedSerializer)
from rest_framework import serializers
from ipam.api.serializers import NestedPrefixSerializer
from dcim.api.serializers import NestedDeviceSerializer
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from ..models import AccessList, AccessListRule
from ..models import AccessList, ACLExtendedRule, ACLStandardRule
#
# Nested serializers
#
class NestedAccessListSerializer(WritableNestedSerializer):
"""
Defines the nested serializer for the django AccessList model & associates it to a view.
"""
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslist-detail'
)
class Meta:
"""
Associates the django model ACLStandardRule & fields to the nested serializer.
"""
model = AccessList
fields = ('id', 'url', 'display', 'name', 'device')
class NestedAccessListRuleSerializer(WritableNestedSerializer):
class NestedACLStandardRuleSerializer(WritableNestedSerializer):
"""
Defines the nested serializer for the django ACLStandardRule model & associates it to a view.
"""
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail'
view_name='plugins-api:netbox_access_lists-api:aclstandardrule-detail'
)
class Meta:
model = AccessListRule
"""
Associates the django model ACLStandardRule & fields to the nested serializer.
"""
model = ACLStandardRule
fields = ('id', 'url', 'display', 'index')
class NestedACLExtendedRuleSerializer(WritableNestedSerializer):
"""
Defines the nested serializer for the django ACLExtendedRule model & associates it to a view.
"""
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:aclextendedrule-detail'
)
class Meta:
"""
Associates the django model ACLExtendedRule & fields to the nested serializer.
"""
model = ACLExtendedRule
fields = ('id', 'url', 'display', 'index')
#
# Regular serializers
#
class AccessListSerializer(NetBoxModelSerializer):
"""
Defines the serializer for the django AccessList model & associates it to a view.
"""
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslist-detail'
)
@ -42,25 +79,90 @@ class AccessListSerializer(NetBoxModelSerializer):
device = NestedDeviceSerializer()
class Meta:
"""
Associates the django model AccessList & fields to the serializer.
"""
model = AccessList
fields = (
'id', 'url', 'display', 'name', 'device', 'type', 'default_action', 'comments', 'tags', 'custom_fields', 'created',
'last_updated', 'rule_count',
'last_updated', 'rule_count'
)
def validate(self, data):
"""
Validate the AccessList django model model's inputs before allowing it to update the instance.
"""
if self.instance.rule_count > 0:
raise serializers.ValidationError({
'type': 'This ACL has ACL rules already associated, CANNOT change ACL type!!'
})
class AccessListRuleSerializer(NetBoxModelSerializer):
return super().validate(data)
class ACLStandardRuleSerializer(NetBoxModelSerializer):
"""
Defines the serializer for the django ACLStandardRule model & associates it to a view.
"""
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail'
view_name='plugins-api:netbox_access_lists-api:aclstandardrule-detail'
)
access_list = NestedAccessListSerializer()
source_prefix = NestedPrefixSerializer()
class Meta:
"""
Associates the django model ACLStandardRule & fields to the serializer.
"""
model = ACLStandardRule
fields = (
'id', 'url', 'display', 'access_list', 'index', 'action', 'tags', 'description',
'created', 'custom_fields', 'last_updated', 'source_prefix'
)
def validate(self, data):
"""
Validate the ACLStandardRule django model model's inputs before allowing it to update the instance.
"""
access_list = data.get('access_list')
if access_list.type == 'extended':
raise serializers.ValidationError({
'access_list': 'CANNOT associated standard ACL rules to an extended ACL!!'
})
return super().validate(data)
class ACLExtendedRuleSerializer(NetBoxModelSerializer):
"""
Defines the serializer for the django ACLExtendedRule model & associates it to a view.
"""
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:aclextendedrule-detail'
)
access_list = NestedAccessListSerializer()
source_prefix = NestedPrefixSerializer()
destination_prefix = NestedPrefixSerializer()
class Meta:
model = AccessListRule
"""
Associates the django model ACLExtendedRule & fields to the serializer.
"""
model = ACLExtendedRule
fields = (
'id', 'url', 'display', 'access_list', 'index', 'protocol', 'source_prefix', 'source_ports',
'destination_prefix', 'destination_ports', 'action', 'tags', 'custom_fields', 'created',
'last_updated',
'id', 'url', 'display', 'access_list', 'index', 'action', 'tags', 'description',
'created', 'custom_fields', 'last_updated', 'source_prefix', 'source_ports',
'destination_prefix', 'destination_ports', 'protocol'
)
def validate(self, data):
"""
Validate the ACLExtendedRule django model model's inputs before allowing it to update the instance.
"""
access_list = data.get('access_list')
if access_list.type == 'standard':
raise serializers.ValidationError({
'access_list': 'CANNOT associated extended ACL rules to a standard ACL!!'
})
return super().validate(data)

View File

@ -1,11 +1,16 @@
from netbox.api.routers import NetBoxRouter
from . import views
"""
Creates API endpoint URLs for the plugin.
"""
from netbox.api.routers import NetBoxRouter
from . import views
app_name = 'netbox_access_list'
router = NetBoxRouter()
router.register('access-lists', views.AccessListViewSet)
router.register('access-list-rules', views.AccessListRuleViewSet)
router.register('standard-acl-rules', views.ACLStandardRuleViewSet)
router.register('extended-acl-rules', views.ACLExtendedRuleViewSet)
urlpatterns = router.urls

View File

@ -1,24 +1,47 @@
from django.db.models import Count
"""
Create views to handle the API logic.
A view set is a single class that can handle the view, add, change,
and delete operations which each require dedicated views under the UI.
"""
from django.db.models import Count
from netbox.api.viewsets import NetBoxModelViewSet
from .. import filtersets, models
from .serializers import AccessListSerializer, AccessListRuleSerializer
from .serializers import (AccessListSerializer, ACLExtendedRuleSerializer,
ACLStandardRuleSerializer)
class AccessListViewSet(NetBoxModelViewSet):
"""
Defines the view set for the django AccessList model & associates it to a view.
"""
queryset = models.AccessList.objects.prefetch_related(
'device', 'tags'
).annotate(
rule_count=Count('rules')
rule_count=Count('aclextendedrules') + Count('aclstandardrules')
)
serializer_class = AccessListSerializer
filterset_class = filtersets.AccessListFilterSet
class AccessListRuleViewSet(NetBoxModelViewSet):
queryset = models.AccessListRule.objects.prefetch_related(
'access_list', 'source_prefix', 'destination_prefix', 'tags'
class ACLStandardRuleViewSet(NetBoxModelViewSet):
"""
Defines the view set for the django ACLStandardRule model & associates it to a view.
"""
queryset = models.ACLStandardRule.objects.prefetch_related(
'access_list', 'tags', 'source_prefix'
)
serializer_class = AccessListRuleSerializer
filterset_class = filtersets.AccessListRuleFilterSet
serializer_class = ACLStandardRuleSerializer
filterset_class = filtersets.ACLStandardRuleFilterSet
class ACLExtendedRuleViewSet(NetBoxModelViewSet):
"""
Defines the view set for the django ACLExtendedRule model & associates it to a view.
"""
queryset = models.ACLExtendedRule.objects.prefetch_related(
'access_list', 'tags', 'source_prefix', 'destination_prefix',
)
serializer_class = ACLExtendedRuleSerializer
filterset_class = filtersets.ACLExtendedRuleFilterSet

View File

@ -0,0 +1,63 @@
"""
Defines the various choices to be used by the models, forms, and other plugin specifics.
"""
from utilities.choices import ChoiceSet
__all__ = (
'ACLActionChoices',
'ACLRuleActionChoices',
'ACLTypeChoices',
'ACLProtocolChoices',
)
class ACLActionChoices(ChoiceSet):
"""
Defines the choices availble for the Access Lists plugin specific to ACL default_action.
"""
ACTION_DENY = 'deny'
ACTION_PERMIT = 'permit'
ACTION_REJECT = 'reject'
CHOICES = [
(ACTION_DENY, 'Deny', 'red'),
(ACTION_PERMIT, 'Permit', 'green'),
(ACTION_REJECT, 'Reject (Reset)', 'orange'),
]
class ACLRuleActionChoices(ChoiceSet):
"""
Defines the choices availble for the Access Lists plugin specific to ACL rule actions.
"""
ACTION_DENY = 'deny'
ACTION_PERMIT = 'permit'
ACTION_REMARK = 'remark'
CHOICES = [
(ACTION_DENY, 'Deny', 'red'),
(ACTION_PERMIT, 'Permit', 'green'),
(ACTION_REMARK, 'Remark', 'blue'),
]
class ACLTypeChoices(ChoiceSet):
"""
Defines the choices availble for the Access Lists plugin specific to ACL type.
"""
CHOICES = [
('extended', 'Extended', 'purple'),
('standard', 'Standard', 'blue'),
]
class ACLProtocolChoices(ChoiceSet):
"""
Defines the choices availble for the Access Lists plugin specific to ACL Rule protocol.
"""
CHOICES = [
('icmp', 'ICMP', 'purple'),
('tcp', 'TCP', 'blue'),
('udp', 'UDP', 'orange'),
]

View File

@ -1,21 +1,71 @@
"""
Filters enable users to request only a specific subset of objects matching a query;
when filtering the sites list by status or region, for instance.
"""
from netbox.filtersets import NetBoxModelFilterSet
from .models import AccessList, AccessListRule
from .models import *
__all__ = (
'AccessListFilterSet',
'ACLStandardRuleFilterSet',
'ACLExtendedRuleFilterSet',
)
class AccessListFilterSet(NetBoxModelFilterSet):
"""
Define the filter set for the django model AccessList.
"""
class Meta:
"""
Associates the django model AccessList & fields to the filter set.
"""
model = AccessList
fields = ('id', 'name', 'device', 'type', 'default_action', 'comments')
def search(self, queryset, name, value):
"""
Override the default search behavior for the django model.
"""
return queryset.filter(description__icontains=value)
class AccessListRuleFilterSet(NetBoxModelFilterSet):
class ACLStandardRuleFilterSet(NetBoxModelFilterSet):
"""
Define the filter set for the django model ACLStandardRule.
"""
class Meta:
model = AccessListRule
fields = ('id', 'access_list', 'index', 'protocol', 'action', 'remark')
"""
Associates the django model ACLStandardRule & fields to the filter set.
"""
model = ACLStandardRule
fields = ('id', 'access_list', 'index', 'action')
def search(self, queryset, name, value):
"""
Override the default search behavior for the django model.
"""
return queryset.filter(description__icontains=value)
class ACLExtendedRuleFilterSet(NetBoxModelFilterSet):
"""
Define the filter set for the django model ACLExtendedRule.
"""
class Meta:
"""
Associates the django model ACLExtendedRule & fields to the filter set.
"""
model = ACLExtendedRule
fields = ('id', 'access_list', 'index', 'action', 'protocol')
def search(self, queryset, name, value):
"""
Override the default search behavior for the django model.
"""
return queryset.filter(description__icontains=value)

View File

@ -1,80 +0,0 @@
from django import forms
from extras.models import Tag
from ipam.models import Prefix
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
from utilities.forms import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField
from .models import AccessList, AccessListRule, AccessListActionChoices, AccessListProtocolChoices, AccessListTypeChoices
class AccessListForm(NetBoxModelForm):
comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = AccessList
fields = ('name', 'device', 'type', 'default_action', 'comments', 'tags')
class AccessListFilterForm(NetBoxModelFilterSetForm):
model = AccessList
type = forms.MultipleChoiceField(
choices=AccessListTypeChoices,
required=False,
widget=StaticSelectMultiple()
)
default_action = forms.MultipleChoiceField(
choices=AccessListActionChoices,
required=False,
widget=StaticSelectMultiple()
)
tag = TagFilterField(model)
class AccessListRuleForm(NetBoxModelForm):
access_list = DynamicModelChoiceField(
queryset=AccessList.objects.all()
)
source_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all()
)
destination_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all()
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = AccessListRule
fields = (
'access_list', 'index', 'remark', 'source_prefix', 'source_ports', 'destination_prefix',
'destination_ports', 'protocol', 'action', 'tags',
)
class AccessListRuleFilterForm(NetBoxModelFilterSetForm):
model = AccessListRule
access_list = forms.ModelMultipleChoiceField(
queryset=AccessList.objects.all(),
required=False,
widget=StaticSelectMultiple()
)
index = forms.IntegerField(
required=False
)
protocol = forms.MultipleChoiceField(
choices=AccessListProtocolChoices,
required=False,
widget=StaticSelectMultiple()
)
action = forms.MultipleChoiceField(
choices=AccessListActionChoices,
required=False,
widget=StaticSelectMultiple()
)
tag = TagFilterField(model)

View File

@ -0,0 +1,13 @@
"""
Import each of the directory's scripts.
"""
#from .bulk_create import *
from .bulk_edit import *
#from .bulk_import import *
#from .connections import *
from .filtersets import *
#from .formsets import *
from .models import *
#from .object_create import *
#from .object_import import *

View File

@ -0,0 +1,86 @@
"""
Draft for a possible BulkEditForm, but may not be worth wile.
"""
from dcim.models import Device, Region, Site, SiteGroup
from django import forms
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from netbox.forms import NetBoxModelBulkEditForm
from utilities.forms import (ChoiceField, DynamicModelChoiceField,
StaticSelect, add_blank_choice)
from ..choices import ACLActionChoices, ACLTypeChoices
from ..models import AccessList
#__all__ = (
# 'AccessListBulkEditForm',
#)
#class AccessListBulkEditForm(NetBoxModelBulkEditForm):
# model = AccessList
#
# region = DynamicModelChoiceField(
# queryset=Region.objects.all(),
# required=False,
# )
# site_group = DynamicModelChoiceField(
# queryset=SiteGroup.objects.all(),
# required=False,
# label='Site Group'
# )
# site = DynamicModelChoiceField(
# queryset=Site.objects.all(),
# required=False
# )
# device = DynamicModelChoiceField(
# queryset=Device.objects.all(),
# query_params={
# 'region': '$region',
# 'group_id': '$site_group',
# 'site_id': '$site',
# },
# required=False,
# )
# type = ChoiceField(
# choices=add_blank_choice(ACLTypeChoices),
# required=False,
# widget=StaticSelect(),
# )
# default_action = ChoiceField(
# choices=add_blank_choice(ACLActionChoices),
# required=False,
# widget=StaticSelect(),
# label='Default Action',
# )
#
# fieldsets = [
# ('Host Details', ('region', 'site_group', 'site', 'device')),
# ('Access List Details', ('type', 'default_action', 'add_tags', 'remove_tags')),
# ]
#
#
# class Meta:
# model = AccessList
# fields = ('region', 'site_group', 'site', 'device', 'type', 'default_action', 'add_tags', 'remove_tags')
# help_texts = {
# 'default_action': 'The default behavior of the ACL.',
# 'name': 'The name uniqueness per device is case insensitive.',
# 'type': mark_safe('<b>*Note:</b> CANNOT be changed if ACL Rules are assoicated to this Access List.'),
# }
#
# def clean(self): # Not working given you are bulkd editing multiple forms
# cleaned_data = super().clean()
# if self.errors.get('name'):
# return cleaned_data
# name = cleaned_data.get('name')
# device = cleaned_data.get('device')
# type = cleaned_data.get('type')
# if ('name' in self.changed_data or 'device' in self.changed_data) and AccessList.objects.filter(name__iexact=name, device=device).exists():
# raise forms.ValidationError('An ACL with this name (case insensitive) is already associated to this device.')
# if type == 'extended' and self.cleaned_data['aclstandardrules'].exists():
# raise forms.ValidationError('This ACL has Standard ACL rules already associated, CANNOT change ACL type!!')
# elif type == 'standard' and self.cleaned_data['aclextendedrules'].exists():
# raise forms.ValidationError('This ACL has Extended ACL rules already associated, CANNOT change ACL type!!')
# return cleaned_data

View File

@ -0,0 +1,134 @@
"""
Defines each django model's GUI filter/search options.
"""
from dcim.models import Device, Region, Site, SiteGroup
from django import forms
from ipam.models import Prefix
from netbox.forms import NetBoxModelFilterSetForm
from utilities.forms import (ChoiceField, DynamicModelChoiceField,
StaticSelect, StaticSelectMultiple,
TagFilterField, add_blank_choice)
from ..choices import (ACLActionChoices, ACLProtocolChoices,
ACLRuleActionChoices, ACLTypeChoices)
from ..models import AccessList, ACLExtendedRule, ACLStandardRule
__all__ = (
'AccessListFilterForm',
'ACLStandardRuleFilterForm',
'ACLExtendedRuleFilterForm',
)
class AccessListFilterForm(NetBoxModelFilterSetForm):
"""
GUI filter form to search the django AccessList model.
"""
model = AccessList
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
)
site_group = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label='Site Group'
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
)
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
query_params={
'region': '$region',
'group_id': '$site_group',
'site_id': '$site',
},
required=False
)
type = ChoiceField(
choices=add_blank_choice(ACLTypeChoices),
required=False,
initial='',
widget=StaticSelect(),
)
default_action = ChoiceField(
choices=add_blank_choice(ACLActionChoices),
required=False,
initial='',
widget=StaticSelect(),
label='Default Action',
)
tag = TagFilterField(model)
fieldsets = (
(None, ('q', 'tag')),
('Host Details', ('region', 'site_group', 'site', 'device')),
('ACL Details', ('type', 'default_action')),
)
class ACLStandardRuleFilterForm(NetBoxModelFilterSetForm):
"""
GUI filter form to search the django ACLStandardRule model.
"""
model = ACLStandardRule
tag = TagFilterField(model)
source_prefix = forms.ModelMultipleChoiceField(
queryset=Prefix.objects.all(),
required=False,
widget=StaticSelectMultiple(),
label='Source Prefix',
)
action = forms.ChoiceField(
choices=add_blank_choice(ACLRuleActionChoices),
required=False,
initial='',
widget=StaticSelect(),
)
fieldsets = (
(None, ('q', 'tag')),
('Rule Details', ('action', 'source_prefix',)),
)
class ACLExtendedRuleFilterForm(NetBoxModelFilterSetForm):
"""
GUI filter form to search the django ACLExtendedRule model.
"""
model = ACLExtendedRule
index = forms.IntegerField(
required=False
)
tag = TagFilterField(model)
action = forms.ChoiceField(
choices=add_blank_choice(ACLRuleActionChoices),
required=False,
widget=StaticSelect(),
initial='',
)
source_prefix = forms.ModelMultipleChoiceField(
queryset=Prefix.objects.all(),
required=False,
widget=StaticSelectMultiple(),
label='Source Prefix',
)
desintation_prefix = forms.ModelMultipleChoiceField(
queryset=Prefix.objects.all(),
required=False,
widget=StaticSelectMultiple(),
label='Destination Prefix',
)
protocol = ChoiceField(
choices=add_blank_choice(ACLProtocolChoices),
required=False,
widget=StaticSelect(),
initial='',
)
fieldsets = (
(None, ('q', 'tag')),
('Rule Details', ('action', 'source_prefix', 'desintation_prefix', 'protocol')),
)

View File

@ -0,0 +1,229 @@
"""
Defines each django model's GUI form to add or edit objects for each django model.
"""
from dcim.models import Device, Region, Site, SiteGroup
from django import forms
from django.utils.safestring import mark_safe
from extras.models import Tag
from ipam.models import Prefix
from netbox.forms import NetBoxModelForm
from utilities.forms import (CommentField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField)
from ..models import AccessList, ACLExtendedRule, ACLStandardRule
__all__ = (
'AccessListForm',
'ACLStandardRuleForm',
'ACLExtendedRuleForm',
)
# Sets a standard mark_save help_text value to be used by the various classes
acl_rule_logic_help = mark_safe('<b>*Note:</b> CANNOT be set if action is set to remark.')
class AccessListForm(NetBoxModelForm):
"""
GUI form to add or edit an AccessList.
Requires a device, a name, a type, and a default_action.
"""
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
)
site_group = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label='Site Group'
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
)
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
query_params={
'region': '$region',
'group_id': '$site_group',
'site_id': '$site',
},
)
comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
fieldsets = [
('Host Details', ('region', 'site_group', 'site', 'device')),
('Access List Details', ('name', 'type', 'default_action', 'tags')),
]
class Meta:
model = AccessList
fields = ('region', 'site_group', 'site', 'device', 'name', 'type', 'default_action', 'comments', 'tags')
help_texts = {
'default_action': 'The default behavior of the ACL.',
'name': 'The name uniqueness per device is case insensitive.',
'type': mark_safe('<b>*Note:</b> CANNOT be changed if ACL Rules are assoicated to this Access List.'),
}
def clean(self):
"""
Validates form inputs before submitting.
"""
cleaned_data = super().clean()
error_message = {}
if self.errors.get('name'):
return cleaned_data
type = cleaned_data.get('type')
if type == 'extended' and self.instance.aclstandardrules.exists():
error_message.update({'type': ['This ACL has Standard ACL rules already associated, CANNOT change ACL type!!']})
elif type == 'standard' and self.instance.aclextendedrules.exists():
error_message.update({'type': ['This ACL has Extended ACL rules already associated, CANNOT change ACL type!!']})
if len(error_message) > 0:
raise forms.ValidationError(error_message)
return cleaned_data
class ACLStandardRuleForm(NetBoxModelForm):
"""
GUI form to add or edit Standard Access List.
Requires an access_list, an index, and ACL rule type.
See the clean function for logic on other field requirements.
"""
access_list = DynamicModelChoiceField(
queryset=AccessList.objects.all(),
query_params={
'type': 'standard'
},
help_text=mark_safe('<b>*Note:</b> This field will only display Standard ACLs.'),
label='Access List',
)
source_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
help_text=acl_rule_logic_help,
label='Source Prefix',
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
fieldsets = (
('Access List Details', ('access_list', 'description', 'tags')),
('Rule Definition', ('index', 'action', 'remark', 'source_prefix')),
)
class Meta:
model = ACLStandardRule
fields = (
'access_list', 'index', 'action', 'remark', 'source_prefix',
'tags', 'description'
)
help_texts = {
'index': 'Determines the order of the rule in the ACL processing.',
'remark': mark_safe('<b>*Note:</b> CANNOT be set if source prefix OR action is set.'),
}
def clean(self):
"""
Validates form inputs before submitting.
If action is set to remark, remark needs to be set.
If action is set to remark, source_prefix cannot be set.
If action is not set to remark, remark cannot be set.
"""
cleaned_data = super().clean()
error_message = {}
if cleaned_data.get('action') == 'remark':
if cleaned_data.get('remark') is None:
error_message.update({'remark': ['Action is set to remark, you MUST add a remark.']})
if cleaned_data.get('source_prefix'):
error_message.update({'source_prefix': ['Action is set to remark, Source Prefix CANNOT be set.']})
elif cleaned_data.get('remark'):
error_message.update({'remark': ['CANNOT set remark unless action is set to remark, .']})
if len(error_message) > 0:
raise forms.ValidationError(error_message)
return cleaned_data
class ACLExtendedRuleForm(NetBoxModelForm):
"""
GUI form to add or edit Extended Access List.
Requires an access_list, an index, and ACL rule type.
See the clean function for logic on other field requirements.
"""
access_list = DynamicModelChoiceField(
queryset=AccessList.objects.all(),
query_params={
'type': 'extended'
},
help_text=mark_safe('<b>*Note:</b> 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,
help_text=acl_rule_logic_help,
label='Source Prefix',
)
destination_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
help_text=acl_rule_logic_help,
label='Destination Prefix',
)
fieldsets = (
('Access List Details', ('access_list', 'description', 'tags')),
('Rule Definition', ('index', 'action', 'remark', 'source_prefix', 'source_ports', 'destination_prefix', 'destination_ports', 'protocol',)),
)
class Meta:
model = ACLExtendedRule
fields = (
'access_list', 'index', 'action', 'remark', 'source_prefix',
'source_ports', 'destination_prefix', 'destination_ports', 'protocol',
'tags', 'description'
)
help_texts = {
'destination_ports': acl_rule_logic_help,
'index': 'Determines the order of the rule in the ACL processing.',
'protocol': acl_rule_logic_help,
'remark': mark_safe('<b>*Note:</b> CANNOT be set if action is not set to remark.'),
'source_ports': acl_rule_logic_help,
}
def clean(self):
"""
Validates form inputs before submitting.
If action is set to remark, remark needs to be set.
If action is set to remark, source_prefix, source_ports, desintation_prefix, destination_ports, or protocol cannot be set.
If action is not set to remark, remark cannot be set.
"""
cleaned_data = super().clean()
error_message = {}
if cleaned_data.get('action') == 'remark':
if cleaned_data.get('remark') is None:
error_message.update({'remark': ['Action is set to remark, you MUST add a remark.']})
if cleaned_data.get('source_prefix'):
error_message.update({'source_prefix': ['Action is set to remark, Source Prefix CANNOT be set.']})
if cleaned_data.get('source_ports'):
error_message.update({'source_ports': ['Action is set to remark, Source Ports CANNOT be set.']})
if cleaned_data.get('destination_prefix'):
error_message.update({'destination_prefix': ['Action is set to remark, Destination Prefix CANNOT be set.']})
if cleaned_data.get('destination_ports'):
error_message.update({'destination_ports': ['Action is set to remark, Destination Ports CANNOT be set.']})
if cleaned_data.get('protocol'):
error_message.update({'protocol': ['Action is set to remark, Protocol CANNOT be set.']})
elif cleaned_data.get('remark'):
error_message.update({'remark': ['CANNOT set remark unless action is set to remark, .']})
if len(error_message) > 0:
raise forms.ValidationError(error_message)
return cleaned_data

View File

@ -1,39 +1,85 @@
"""
Define the object types and queries availble via the graphql api.
"""
from graphene import ObjectType
from netbox.graphql.types import NetBoxObjectType
from netbox.graphql.fields import ObjectField, ObjectListField
from netbox.graphql.types import NetBoxObjectType
from . import filtersets, models
__all__ = (
'AccessListType',
'ACLExtendedRuleType',
'ACLStandardRuleType',
)
#
# Object types
#
class AccessListType(NetBoxObjectType):
"""
Defines the object type for the django model AccessList.
"""
class Meta:
"""
Associates the filterset, fields, and model for the django model AccessList.
"""
model = models.AccessList
fields = '__all__'
filterset_class = filtersets.AccessListFilterSet
class AccessListRuleType(NetBoxObjectType):
class ACLExtendedRuleType(NetBoxObjectType):
"""
Defines the object type for the django model ACLExtendedRule.
"""
class Meta:
model = models.AccessListRule
"""
Associates the filterset, fields, and model for the django model ACLExtendedRule.
"""
model = models.ACLExtendedRule
fields = '__all__'
filterset_class = filtersets.AccessListRuleFilterSet
filterset_class = filtersets.ACLExtendedRuleFilterSet
class ACLStandardRuleType(NetBoxObjectType):
"""
Defines the object type for the django model ACLStandardRule.
"""
class Meta:
"""
Associates the filterset, fields, and model for the django model ACLStandardRule.
"""
model = models.ACLStandardRule
fields = '__all__'
filterset_class = filtersets.ACLStandardRuleFilterSet
#
# Queries
#
class Query(ObjectType):
"""
Defines the queries availible to this plugin via the graphql api.
"""
access_list = ObjectField(AccessListType)
access_lists = ObjectListField(AccessListType)
access_list_list = ObjectListField(AccessListType)
access_list_rule = ObjectField(AccessListRuleType)
access_list_rule_list = ObjectListField(AccessListRuleType)
acl_extended_rule = ObjectField(ACLExtendedRuleType)
acl_extended_rules = ObjectListField(ACLExtendedRuleType)
acl_extended_rule_list = ObjectListField(ACLExtendedRuleType)
acl_standard_rule = ObjectField(ACLStandardRuleType)
acl_standard_rules = ObjectListField(ACLStandardRuleType)
acl_standard_rule_list = ObjectListField(ACLStandardRuleType)
schema = Query

View File

@ -1,11 +1,21 @@
"""
Defines the migrations for propogating django models into the database schemea.
"""
import django.contrib.postgres.fields
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
__all__ = (
'Migration',
)
class Migration(migrations.Migration):
"""
Defines the migrations required for the initial setup of the access lists plugin and its associated django models.
"""
initial = True
@ -25,36 +35,60 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=100)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_lists', to='dcim.device')),
('type', models.CharField(max_length=100)),
('default_action', models.CharField(max_length=30)),
('default_action', models.CharField(default='deny', max_length=30)),
('comments', models.TextField(blank=True)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('name', 'device'),
'unique_together': {('name', 'device')},
'verbose_name': 'Access List',
},
),
migrations.CreateModel(
name='AccessListRule',
name='ACLStandardRule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
('access_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aclstandardrules', to='netbox_access_lists.accesslist')),
('index', models.PositiveIntegerField()),
('protocol', models.CharField(blank=True, max_length=30)),
('source_ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, null=True, size=None)),
('destination_ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, null=True, size=None)),
('description', models.CharField(blank=True, max_length=500)),
('action', models.CharField(max_length=30)),
('remark', models.CharField(blank=True, null=True, max_length=500)),
('access_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='netbox_access_lists.accesslist')),
('destination_prefix', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ipam.prefix')),
('source_prefix', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ipam.prefix')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('access_list', 'index'),
'unique_together': {('access_list', 'index')},
'verbose_name': 'ACL Standard Rule',
},
),
migrations.CreateModel(
name='ACLExtendedRule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
('access_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aclstandardrules', to='netbox_access_lists.accesslist')),
('index', models.PositiveIntegerField()),
('description', models.CharField(blank=True, max_length=500)),
('action', models.CharField(max_length=30)),
('remark', models.CharField(blank=True, null=True, max_length=500)),
('source_prefix', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ipam.prefix')),
('source_ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, null=True, size=None)),
('destination_prefix', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ipam.prefix')),
('destination_ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, null=True, size=None)),
('protocol', models.CharField(blank=True, max_length=30)),
],
options={
'ordering': ('access_list', 'index'),
'unique_together': {('access_list', 'index')},
'verbose_name': 'ACL Extended Rule',
},
),
]

View File

@ -1,144 +0,0 @@
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.urls import reverse
from netbox.models import NetBoxModel
from utilities.choices import ChoiceSet
class AccessListActionChoices(ChoiceSet):
key = 'AccessListRule.action'
ACTION_DENY = 'deny'
ACTION_PERMIT = 'permit'
ACTION_REJECT = 'reject'
CHOICES = [
(ACTION_DENY, 'Deny', 'red'),
(ACTION_PERMIT, 'Permit', 'green'),
(ACTION_REJECT, 'Reject (Reset)', 'orange'),
]
class AccessListTypeChoices(ChoiceSet):
CHOICES = [
('extended', 'Extended', 'purple'),
('standard', 'Standard', 'blue'),
]
class AccessListProtocolChoices(ChoiceSet):
CHOICES = [
('icmp', 'ICMP', 'purple'),
('tcp', 'TCP', 'blue'),
('udp', 'UDP', 'orange'),
]
class AccessList(NetBoxModel):
name = models.CharField(
max_length=100
)
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='access_lists'
)
type = models.CharField(
max_length=30,
choices=AccessListTypeChoices
)
default_action = models.CharField(
default=AccessListActionChoices.ACTION_DENY,
max_length=30,
choices=AccessListActionChoices,
verbose_name='Default Action'
)
comments = models.TextField(
blank=True
)
class Meta:
ordering = ('name', 'device')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('plugins:netbox_access_lists:accesslist', args=[self.pk])
def get_default_action_color(self):
return AccessListActionChoices.colors.get(self.default_action)
def get_type_color(self):
return AccessListTypeChoices.colors.get(self.type)
class AccessListRule(NetBoxModel):
access_list = models.ForeignKey(
on_delete=models.CASCADE,
related_name='rules',
to=AccessList,
verbose_name='Access List'
)
index = models.PositiveIntegerField()
protocol = models.CharField(
blank=True,
choices=AccessListProtocolChoices,
max_length=30,
)
source_prefix = models.ForeignKey(
blank=True,
null=True,
on_delete=models.PROTECT,
related_name='+',
to='ipam.Prefix',
verbose_name='Source Prefix'
)
source_ports = ArrayField(
base_field=models.PositiveIntegerField(),
blank=True,
null=True,
verbose_name='Soure Ports'
)
destination_prefix = models.ForeignKey(
blank=True,
null=True,
on_delete=models.PROTECT,
related_name='+',
to='ipam.Prefix',
verbose_name='Destination Prefix'
)
destination_ports = ArrayField(
base_field=models.PositiveIntegerField(),
blank=True,
null=True,
verbose_name='Destination Ports'
)
action = models.CharField(
choices=AccessListActionChoices,
default=AccessListActionChoices.ACTION_PERMIT,
max_length=30,
)
remark = models.CharField(
max_length=200,
blank=True,
null=True
)
class Meta:
ordering = ('access_list', 'index')
unique_together = ('access_list', 'index')
def __str__(self):
return f'{self.access_list}: Rule {self.index}'
def get_absolute_url(self):
return reverse('plugins:netbox_access_lists:accesslistrule', args=[self.pk])
def get_protocol_color(self):
return AccessListProtocolChoices.colors.get(self.protocol)
def get_action_color(self):
return AccessListActionChoices.colors.get(self.action)

View File

@ -0,0 +1,6 @@
"""
Import each of the directory's scripts.
"""
from .access_list_rules import *
from .access_lists import *

View File

@ -0,0 +1,144 @@
"""
Define the django models for this plugin.
"""
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.urls import reverse
from netbox.models import NetBoxModel
from ..choices import *
from .access_lists import AccessList
__all__ = (
'ACLRule',
'ACLStandardRule',
'ACLExtendedRule',
)
class ACLRule(NetBoxModel):
"""
Abstract model for ACL Rules.
Inherrited by both ACLStandardRule and ACLExtendedRule.
"""
access_list = models.ForeignKey(
on_delete=models.CASCADE,
to=AccessList,
verbose_name='Access List',
)
index = models.PositiveIntegerField()
remark = models.CharField(
max_length=200,
blank=True,
null=True
)
description = models.CharField(
max_length=500,
blank=True
)
action = models.CharField(
choices=ACLRuleActionChoices,
max_length=30,
)
source_prefix = models.ForeignKey(
blank=True,
null=True,
on_delete=models.PROTECT,
related_name='+',
to='ipam.Prefix',
verbose_name='Source Prefix'
)
def __str__(self):
return f'{self.access_list}: Rule {self.index}'
def get_action_color(self):
return ACLRuleActionChoices.colors.get(self.action)
class Meta:
"""
Define the common model properties:
- as an abstract model
- ordering
- unique together
"""
abstract = True
ordering = ('access_list', 'index')
unique_together = ('access_list', 'index')
class ACLStandardRule(ACLRule):
"""
Inherits ACLRule.
"""
def get_absolute_url(self):
"""
The method is a Django convention; although not strictly required,
it conveniently returns the absolute URL for any particular object.
"""
return reverse('plugins:netbox_access_lists:aclstandardrule', args=[self.pk])
class Meta(ACLRule.Meta):
"""
Define the model properties adding to or overriding the inherited class:
- default_related_name for any FK relationships
- verbose name (for displaying in the GUI)
- verbose name plural (for displaying in the GUI)
"""
default_related_name='aclstandardrules'
verbose_name='ACL Standard Rule'
verbose_name_plural='ACL Standard Rules'
class ACLExtendedRule(ACLRule):
"""
Inherits ACLRule.
Add ACLExtendedRule specific fields: source_ports, desintation_prefix, destination_ports, and protocol
"""
source_ports = ArrayField(
base_field=models.PositiveIntegerField(),
blank=True,
null=True,
verbose_name='Soure Ports'
)
destination_prefix = models.ForeignKey(
blank=True,
null=True,
on_delete=models.PROTECT,
related_name='+',
to='ipam.Prefix',
verbose_name='Destination Prefix'
)
destination_ports = ArrayField(
base_field=models.PositiveIntegerField(),
blank=True,
null=True,
verbose_name='Destination Ports'
)
protocol = models.CharField(
blank=True,
choices=ACLProtocolChoices,
max_length=30,
)
def get_absolute_url(self):
"""
The method is a Django convention; although not strictly required,
it conveniently returns the absolute URL for any particular object.
"""
return reverse('plugins:netbox_access_lists:aclextendedrule', args=[self.pk])
def get_protocol_color(self):
return ACLProtocolChoices.colors.get(self.protocol)
class Meta(ACLRule.Meta):
"""
Define the model properties adding to or overriding the inherited class:
- default_related_name for any FK relationships
- verbose name (for displaying in the GUI)
- verbose name plural (for displaying in the GUI)
"""
default_related_name='aclextendedrules'
verbose_name='ACL Extended Rule'
verbose_name_plural='ACL Extended Rules'

View File

@ -0,0 +1,63 @@
"""
Define the django models for this plugin.
"""
from django.db import models
from django.urls import reverse
from netbox.models import NetBoxModel
from ..choices import *
__all__ = (
'AccessList',
)
class AccessList(NetBoxModel):
"""
Model defintion for Access Lists.
"""
name = models.CharField(
max_length=100
)
device = models.ForeignKey(
to='dcim.Device',
on_delete=models.CASCADE,
related_name='access_lists'
)
type = models.CharField(
max_length=30,
choices=ACLTypeChoices
)
default_action = models.CharField(
default=ACLActionChoices.ACTION_DENY,
max_length=30,
choices=ACLActionChoices,
verbose_name='Default Action'
)
comments = models.TextField(
blank=True
)
class Meta:
ordering = ('name', 'device')
unique_together = ('name', 'device')
default_related_name='accesslists'
verbose_name='Access List'
verbose_name_plural='Access Lists'
def __str__(self):
return self.name
def get_absolute_url(self):
"""
The method is a Django convention; although not strictly required,
it conveniently returns the absolute URL for any particular object.
"""
return reverse('plugins:netbox_access_lists:accesslist', args=[self.pk])
def get_default_action_color(self):
return ACLActionChoices.colors.get(self.default_action)
def get_type_color(self):
return ACLTypeChoices.colors.get(self.type)

View File

@ -1,6 +1,13 @@
"""
Define the plugin menu buttons & the plugin navigation bar enteries.
"""
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
#
# Define plugin menu buttons
#
accesslist_buttons = [
PluginMenuButton(
@ -11,25 +18,44 @@ accesslist_buttons = [
)
]
accesslistrule_butons = [
aclstandardrule_butons = [
PluginMenuButton(
link='plugins:netbox_access_lists:accesslistrule_add',
link='plugins:netbox_access_lists:aclstandardrule_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN
)
]
aclextendedrule_butons = [
PluginMenuButton(
link='plugins:netbox_access_lists:aclextendedrule_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN
)
]
#
# Define navigation bar links including the above buttons defined.
#
menu_items = (
PluginMenuItem(
link='plugins:netbox_access_lists:accesslist_list',
link_text='Access Lists',
buttons=accesslist_buttons
),
# # Comment out Access List Rule to force creation in the ACL view
# PluginMenuItem(
# link='plugins:netbox_access_lists:accesslistrule_list',
# link_text='Access List Rules',
# buttons=accesslistrule_butons
# ),
# Comment out Standard Access List rule to force creation in the ACL view
PluginMenuItem(
link='plugins:netbox_access_lists:aclstandardrule_list',
link_text='ACL Standard Rule',
buttons=aclstandardrule_butons
),
# Comment out Extended Access List rule to force creation in the ACL view
PluginMenuItem(
link='plugins:netbox_access_lists:aclextendedrule_list',
link_text='ACL Extended Rules',
buttons=aclextendedrule_butons
),
)

View File

@ -1,10 +1,23 @@
import django_tables2 as tables
"""
Define the object lists / table view for each of the plugin models.
"""
from netbox.tables import NetBoxTable, columns, ChoiceFieldColumn
from .models import AccessList, AccessListRule
import django_tables2 as tables
from netbox.tables import ChoiceFieldColumn, NetBoxTable, columns
from .models import AccessList, ACLExtendedRule, ACLStandardRule
__all__ = (
'AccessListTable',
'ACLStandardRuleTable',
'ACLExtendedRuleTable',
)
class AccessListTable(NetBoxTable):
"""
Defines the table view for the AccessList model.
"""
name = tables.Column(
linkify=True
)
@ -26,26 +39,54 @@ class AccessListTable(NetBoxTable):
default_columns = ('name', 'device', 'type', 'rule_count', 'default_action', 'tags')
class AccessListRuleTable(NetBoxTable):
class ACLStandardRuleTable(NetBoxTable):
"""
Defines the table view for the ACLStandardRule model.
"""
access_list = tables.Column(
linkify=True
)
index = tables.Column(
linkify=True
)
protocol = ChoiceFieldColumn()
action = ChoiceFieldColumn()
tags = columns.TagColumn(
url_name='plugins:netbox_access_lists:accesslistrule_list'
url_name='plugins:netbox_access_lists:aclstandardrule_list'
)
class Meta(NetBoxTable.Meta):
model = AccessListRule
model = ACLStandardRule
fields = (
'pk', 'id', 'access_list', 'index', 'source_prefix', 'source_ports', 'destination_prefix',
'destination_ports', 'protocol', 'action', 'remark', 'actions', 'tags'
'pk', 'id', 'access_list', 'index', 'action', 'actions', 'remark', 'tags', 'description',
)
default_columns = (
'access_list', 'index', 'remark', 'source_prefix', 'source_ports', 'destination_prefix',
'destination_ports', 'protocol', 'action', 'actions', 'tags'
'access_list', 'index', 'action', 'actions', 'remark', 'tags'
)
class ACLExtendedRuleTable(NetBoxTable):
"""
Defines the table view for the ACLExtendedRule model.
"""
access_list = tables.Column(
linkify=True
)
index = tables.Column(
linkify=True
)
action = ChoiceFieldColumn()
tags = columns.TagColumn(
url_name='plugins:netbox_access_lists:aclextendedrule_list'
)
protocol = ChoiceFieldColumn()
class Meta(NetBoxTable.Meta):
model = ACLExtendedRule
fields = (
'pk', 'id', 'access_list', 'index', 'action', 'actions', 'remark', 'tags', 'description',
'source_prefix', 'source_ports', 'destination_prefix', 'destination_ports', 'protocol'
)
default_columns = (
'access_list', 'index', 'action', 'actions', 'remark', 'tags',
'source_prefix', 'source_ports', 'destination_prefix', 'destination_ports', 'protocol'
)

View File

@ -1,9 +1,14 @@
from django.contrib.contenttypes.models import ContentType
from extras.plugins import PluginTemplateExtension
from .models import AccessList
__all__ = (
'AccessLists',
'DeviceAccessLists',
)
class AccessLists(PluginTemplateExtension):
@ -17,7 +22,7 @@ class AccessLists(PluginTemplateExtension):
#elif ctype.model == 'virtualmachine':
# access_lists = AccessList.objects.filter(device=obj.pk)
return self.render('inc/device_access_lists.html', extra_context={
return self.render('inc/device/access_lists.html', extra_context={
'access_lists': access_lists,
'type': ctype.model if ctype.model == 'device' else ctype.name.replace(' ', '_'),
})

View File

@ -3,7 +3,7 @@
Access Lists
</h5>
<div class="card-body">
{% include 'inc/assigned_access_lists.html' %}
{% include 'inc/device/assigned_access_lists.html' %}
</div>
<div class="card-footer text-end noprint">
<a href="{% url 'plugins:netbox_access_lists:accesslist_add' %}?{{ type }}={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">

View File

@ -12,7 +12,11 @@
<td>{{ access_list|linkify }}</td>
<td>{{ access_list.type|title }}</td>
<td>{{ access_list.default_action|title }}</td>
<td>{{ access_list.rules.count }}</td>
{% if access_list.type == 'standard' %}
<td>{{ access_list.aclstandardrules.count|placeholder }}</td>
{% elif access_list.type == 'extended' %}
<td>{{ access_list.aclextendedrules.count|placeholder }}</td>
{% endif %}
</tr>
{% endfor %}
</table>

View File

@ -7,7 +7,11 @@
{% block controls %}
<div class="pull-right noprint">
{% if perms.netbox_access_lists.change_policy %}
<a href="{% url 'plugins:netbox_access_lists:accesslistrule_add' %}?access_list={{ object.pk }}" class="btn btn-success">
{% if object.type == 'extended' %}
<a href="{% url 'plugins:netbox_access_lists:aclextendedrule_add' %}?access_list={{ object.pk }}" class="btn btn-success">
{% elif object.type == 'standard' %}
<a href="{% url 'plugins:netbox_access_lists:aclstandardrule_add' %}?access_list={{ object.pk }}" class="btn btn-success">
{% endif %}
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Rule
</a>
{% endif %}
@ -45,10 +49,6 @@
<h5 class="card-header">Access List</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.get_type_display }}</td>
@ -59,7 +59,15 @@
</tr>
<tr>
<th scope="row">Rules</th>
<td>{{ object.rules.count }}</td>
{% if object.type == 'standard' %}
<td>{{ object.aclstandardrules.count|placeholder }}</td>
{% elif object.type == 'extended' %}
<td>{{ object.aclextendedrules.count|placeholder }}</td>
{% endif %}
<tr>
<th scope="row">Device</th>
<td>{{ object.device|linkify }}</td>
</tr>
</tr>
</table>
</div>
@ -74,7 +82,7 @@
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Rules</h5>
<h5 class="card-header">{{ object.get_type_display }} Rules</h5>
<div class="card-body table-responsive">
{% render_table rules_table %}
</div>

View File

@ -4,7 +4,7 @@
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Access List Rule</h5>
<h5 class="card-header">ACL Extended Rule</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
@ -13,6 +13,10 @@
<a href="{{ object.access_list.get_absolute_url }}">{{ object.access_list }}</a>
</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Index</th>
<td>{{ object.index }}</td>

View File

@ -0,0 +1,62 @@
{% extends 'generic/object.html' %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">ACL Standard Rule</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Access List</th>
<td>
<a href="{{ object.access_list.get_absolute_url }}">{{ object.access_list }}</a>
</td>
</tr>
<tr>
<th scope="row">Index</th>
<td>{{ object.index }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Details</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Remark</th>
<td>{{ object.get_remark_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Protocol</th>
<td>{{ object.get_protocol_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Source Prefix</th>
<td>
{% if object.source_prefix %}
<a href="{{ object.source_prefix.get_absolute_url }}">{{ object.source_prefix }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Action</th>
<td>{{ object.get_action_display }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -1,14 +1,18 @@
"""
Map Views to URLs.
"""
from django.urls import path
from netbox.views.generic import ObjectChangeLogView
from . import models, views
from . import models, views
urlpatterns = (
# Access lists
# Access Lists
path('access-lists/', views.AccessListListView.as_view(), name='accesslist_list'),
path('access-lists/add/', views.AccessListEditView.as_view(), name='accesslist_add'),
#path('access-lists/edit/', views.AccessListBulkEditView.as_view(), name='accesslist_bulk_edit'),
path('access-lists/<int:pk>/', views.AccessListView.as_view(), name='accesslist'),
path('access-lists/<int:pk>/edit/', views.AccessListEditView.as_view(), name='accesslist_edit'),
path('access-lists/<int:pk>/delete/', views.AccessListDeleteView.as_view(), name='accesslist_delete'),
@ -16,14 +20,24 @@ urlpatterns = (
'model': models.AccessList
}),
# Access list rules
path('rules/', views.AccessListRuleListView.as_view(), name='accesslistrule_list'),
path('rules/add/', views.AccessListRuleEditView.as_view(), name='accesslistrule_add'),
path('rules/<int:pk>/', views.AccessListRuleView.as_view(), name='accesslistrule'),
path('rules/<int:pk>/edit/', views.AccessListRuleEditView.as_view(), name='accesslistrule_edit'),
path('rules/<int:pk>/delete/', views.AccessListRuleDeleteView.as_view(), name='accesslistrule_delete'),
path('rules/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='accesslistrule_changelog', kwargs={
'model': models.AccessListRule
# Standard Access List rules
path('standard-rules/', views.ACLStandardRuleListView.as_view(), name='aclstandardrule_list'),
path('standard-rules/add/', views.ACLStandardRuleEditView.as_view(), name='aclstandardrule_add'),
path('standard-rules/<int:pk>/', views.ACLStandardRuleView.as_view(), name='aclstandardrule'),
path('standard-rules/<int:pk>/edit/', views.ACLStandardRuleEditView.as_view(), name='aclstandardrule_edit'),
path('standard-rules/<int:pk>/delete/', views.ACLStandardRuleDeleteView.as_view(), name='aclstandardrule_delete'),
path('standard-rules/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aclstandardrule_changelog', kwargs={
'model': models.ACLStandardRule
}),
# Extended Access List rules
path('extended-rules/', views.ACLExtendedRuleListView.as_view(), name='aclextendedrule_list'),
path('extended-rules/add/', views.ACLExtendedRuleEditView.as_view(), name='aclextendedrule_add'),
path('extended-rules/<int:pk>/', views.ACLExtendedRuleView.as_view(), name='aclextendedrule'),
path('extended-rules/<int:pk>/edit/', views.ACLExtendedRuleEditView.as_view(), name='aclextendedrule_edit'),
path('extended-rules/<int:pk>/delete/', views.ACLExtendedRuleDeleteView.as_view(), name='aclextendedrule_delete'),
path('extended-rules/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aclextendedrule_changelog', kwargs={
'model': models.ACLExtendedRule
}),
)

View File

@ -1 +1 @@
__version__ = "0.0.0"
__version__ = "1.0.0"

View File

@ -1,28 +1,59 @@
from django.db.models import Count
"""
Defines the business logic for the plugin.
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
__all__ = (
'AccessListView',
'AccessListListView',
'AccessListEditView',
'ACLStandardRuleView',
'ACLStandardRuleListView',
'ACLStandardRuleEditView',
'ACLStandardRuleDeleteView',
'ACLExtendedRuleView',
'ACLExtendedRuleListView',
'ACLExtendedRuleEditView',
'ACLExtendedRuleDeleteView',
)
#
# AccessList views
#
class AccessListView(generic.ObjectView):
"""
Defines the view for the AccessLists django model.
"""
queryset = models.AccessList.objects.all()
def get_extra_context(self, request, instance):
table = tables.AccessListRuleTable(instance.rules.all())
"""
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,
'rules_table': table
}
class AccessListListView(generic.ObjectListView):
"""
Defines the list view for the AccessLists django model.
"""
queryset = models.AccessList.objects.annotate(
rule_count=Count('rules')
rule_count=Count('aclextendedrules') + Count('aclstandardrules')
)
table = tables.AccessListTable
filterset = filtersets.AccessListFilterSet
@ -30,33 +61,99 @@ class AccessListListView(generic.ObjectListView):
class AccessListEditView(generic.ObjectEditView):
"""
Defines the edit view for the AccessLists django model.
"""
queryset = models.AccessList.objects.all()
form = forms.AccessListForm
class AccessListDeleteView(generic.ObjectDeleteView):
"""
Defines the delete view for the AccessLists django model.
"""
queryset = models.AccessList.objects.all()
#class AccessListBulkEditView(generic.BulkEditView):
# """
# Defines the bulk edit view for the AccessList django model.
# """
# queryset = models.AccessList.objects.annotate(
# rule_count=Count('aclextendedrules') + Count('aclstandardrules')
# )
# table = tables.AccessListTable
# filterset = filtersets.AccessListFilterSet
# form = forms.AccessListBulkEditForm
#
# AccessListRule views
# ACLStandardRule views
#
class AccessListRuleView(generic.ObjectView):
queryset = models.AccessListRule.objects.all()
class ACLStandardRuleView(generic.ObjectView):
"""
Defines the view for the ACLStandardRule django model.
"""
queryset = models.ACLStandardRule.objects.all()
class AccessListRuleListView(generic.ObjectListView):
queryset = models.AccessListRule.objects.all()
table = tables.AccessListRuleTable
filterset = filtersets.AccessListRuleFilterSet
filterset_form = forms.AccessListRuleFilterForm
class ACLStandardRuleListView(generic.ObjectListView):
"""
Defines the list view for the ACLStandardRule django model.
"""
queryset = models.ACLStandardRule.objects.all()
table = tables.ACLStandardRuleTable
filterset = filtersets.ACLStandardRuleFilterSet
filterset_form = forms.ACLStandardRuleFilterForm
class AccessListRuleEditView(generic.ObjectEditView):
queryset = models.AccessListRule.objects.all()
form = forms.AccessListRuleForm
class ACLStandardRuleEditView(generic.ObjectEditView):
"""
Defines the edit view for the ACLStandardRule django model.
"""
queryset = models.ACLStandardRule.objects.all()
form = forms.ACLStandardRuleForm
class AccessListRuleDeleteView(generic.ObjectDeleteView):
queryset = models.AccessListRule.objects.all()
class ACLStandardRuleDeleteView(generic.ObjectDeleteView):
"""
Defines the delete view for the ACLStandardRules django model.
"""
queryset = models.ACLStandardRule.objects.all()
#
# ACLExtendedRule views
#
class ACLExtendedRuleView(generic.ObjectView):
"""
Defines the view for the ACLExtendedRule django model.
"""
queryset = models.ACLExtendedRule.objects.all()
class ACLExtendedRuleListView(generic.ObjectListView):
"""
Defines the list view for the ACLExtendedRule django model.
"""
queryset = models.ACLExtendedRule.objects.all()
table = tables.ACLExtendedRuleTable
filterset = filtersets.ACLExtendedRuleFilterSet
filterset_form = forms.ACLExtendedRuleFilterForm
class ACLExtendedRuleEditView(generic.ObjectEditView):
"""
Defines the edit view for the ACLExtendedRule django model.
"""
queryset = models.ACLExtendedRule.objects.all()
form = forms.ACLExtendedRuleForm
class ACLExtendedRuleDeleteView(generic.ObjectDeleteView):
"""
Defines the delete view for the ACLExtendedRules django model.
"""
queryset = models.ACLExtendedRule.objects.all()

View File

@ -1,3 +1,7 @@
[MASTER]
load-plugins=pylint_django
django-settings-module=/opt/netbox/netbox/netbox/netbox.settings
[tool.black]
line-length = 100
@ -25,5 +29,27 @@ disable = [
"unsubscriptable-object",
]
enable = [
"expression-not-assigned",
"global-variable-not-assigned",
"possibly-unused-variable",
"reimported",
"unused-argument",
"unused-import",
"unused-variable",
"unused-wildcard-import",
"useless-else-on-loop",
"useless-import-alias",
"useless-object-inheritance",
"useless-parent-delegation",
"useless-return",
"useless-suppression", # Identify unneeded pylint disable statements
]
[tool.isort]
force_grid_wrap = 0
include_trailing_comma = true
line_length = 79
multi_line_output = 1
overwrite_in_place = true
use_parentheses = true
verbose = true

View File

@ -2,7 +2,7 @@
#import os.path
#
from setuptools import find_packages, setup
#
#
#with open("README.md", "r") as fh:
# long_description = fh.read()
@ -27,7 +27,7 @@ setup(
name='netbox-access-lists',
version='0.1.0',
#version=get_version('netbox_access_lists/version.py'),
description='A NetBox plugin for Access-List management',
description='A NetBox plugin for Access List management',
#long_description=long_description,
long_description_content_type="text/markdown",
url='https://github.com/ryanmerolle/netbox-access-lists',