├── netbox_acls
├── api
│ ├── __init__.py
│ ├── urls.py
│ └── views.py
├── tests
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── test_app.py
│ │ ├── test_access_lists.py
│ │ └── test_access_list_rules.py
│ └── models
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── test_aclinterfaceassignments.py
│ │ └── test_standardrules.py
├── migrations
│ ├── __init__.py
│ ├── 0004_netbox_acls.py
│ ├── 0003_netbox_acls.py
│ └── 0002_alter_accesslist_options_and_more.py
├── CODEOWNERS
├── graphql
│ ├── __init__.py
│ ├── filters
│ │ ├── __init__.py
│ │ ├── access_lists.py
│ │ └── access_list_rules.py
│ ├── enums.py
│ ├── schema.py
│ └── types.py
├── models
│ ├── __init__.py
│ ├── access_lists.py
│ └── access_list_rules.py
├── forms
│ ├── __init__.py
│ ├── bulk_edit.py
│ └── filtersets.py
├── constants.py
├── __init__.py
├── templates
│ ├── inc
│ │ └── view_tab.html
│ └── netbox_acls
│ │ ├── aclinterfaceassignment.html
│ │ ├── aclstandardrule.html
│ │ ├── aclextendedrule.html
│ │ └── accesslist.html
├── urls.py
├── choices.py
├── navigation.py
├── tables.py
└── filtersets.py
├── env
├── redis.env
├── postgres.env
└── netbox.env
├── .devcontainer
├── env
│ ├── redis.env
│ ├── postgres.env
│ └── netbox.env
├── requirements-dev.txt
├── docker-compose.override.yml
├── configuration
│ ├── plugins.py
│ └── logging.py
├── entrypoint-dev.sh
├── Dockerfile-plugin_dev
├── docker-compose.yml
├── .bashrc
├── .zshrc
└── devcontainer.json
├── .flake8
├── .jscpd.json
├── MANIFEST.in
├── docs
└── img
│ ├── access_lists.png
│ ├── acl_host_view.png
│ ├── acl_extended_rules.png
│ ├── acl_interface_view.png
│ ├── acl_standard_rules.png
│ ├── access_list_type_extended.png
│ ├── access_list_type_standard.png
│ └── acl_interface_assignments.png
├── SECURITY.md
├── Dockerfile
├── configuration
├── plugins.py
├── logging.py
└── configuration.py
├── .github
├── workflows
│ ├── greetings.yml
│ ├── dependency-review.yml
│ ├── python-publish.yml
│ ├── ci.yml
│ └── codeql-analysis.yml
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── housekeeping.yml
│ ├── documentation_change.yml
│ ├── bug_report.yml
│ └── feature_request.yml
└── pull_request_template.md
├── TODO
├── docker-compose.yml
├── test.sh
├── ruff.toml
├── pyproject.toml
├── .pre-commit-config.yaml
├── .gitignore
├── Makefile
├── CONTRIBUTING.md
├── README.md
└── LICENSE.txt
/netbox_acls/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/netbox_acls/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/netbox_acls/tests/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/netbox_acls/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/netbox_acls/tests/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/env/redis.env:
--------------------------------------------------------------------------------
1 | REDIS_PASSWORD=H733Kdjndks81
2 |
--------------------------------------------------------------------------------
/.devcontainer/env/redis.env:
--------------------------------------------------------------------------------
1 | REDIS_PASSWORD=H733Kdjndks81
2 |
--------------------------------------------------------------------------------
/netbox_acls/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @ryanmerolle @abhi1693 @cruse1977 @natm
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 140
3 | extend-ignore = E203
4 |
--------------------------------------------------------------------------------
/.jscpd.json:
--------------------------------------------------------------------------------
1 | {
2 | "threshold": 10,
3 | "ignore": ["**/migrations/**", "**/tests/**"]
4 | }
5 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include LICENSE
3 | recursive-include netbox_acls/templates *
4 |
--------------------------------------------------------------------------------
/env/postgres.env:
--------------------------------------------------------------------------------
1 | POSTGRES_DB=netbox
2 | POSTGRES_PASSWORD=J5brHrAXFLQSif0K
3 | POSTGRES_USER=netbox
4 |
--------------------------------------------------------------------------------
/netbox_acls/graphql/__init__.py:
--------------------------------------------------------------------------------
1 | from .schema import NetBoxACLSQuery
2 |
3 | schema = [NetBoxACLSQuery]
4 |
--------------------------------------------------------------------------------
/.devcontainer/env/postgres.env:
--------------------------------------------------------------------------------
1 | POSTGRES_DB=netbox
2 | POSTGRES_PASSWORD=J5brHrAXFLQSif0K
3 | POSTGRES_USER=netbox
4 |
--------------------------------------------------------------------------------
/docs/img/access_lists.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-acls/HEAD/docs/img/access_lists.png
--------------------------------------------------------------------------------
/docs/img/acl_host_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-acls/HEAD/docs/img/acl_host_view.png
--------------------------------------------------------------------------------
/docs/img/acl_extended_rules.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-acls/HEAD/docs/img/acl_extended_rules.png
--------------------------------------------------------------------------------
/docs/img/acl_interface_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-acls/HEAD/docs/img/acl_interface_view.png
--------------------------------------------------------------------------------
/docs/img/acl_standard_rules.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-acls/HEAD/docs/img/acl_standard_rules.png
--------------------------------------------------------------------------------
/docs/img/access_list_type_extended.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-acls/HEAD/docs/img/access_list_type_extended.png
--------------------------------------------------------------------------------
/docs/img/access_list_type_standard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-acls/HEAD/docs/img/access_list_type_standard.png
--------------------------------------------------------------------------------
/docs/img/acl_interface_assignments.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-acls/HEAD/docs/img/acl_interface_assignments.png
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | Feel free to raise an issue about any vunerabilities introduced in this plugin.
6 |
--------------------------------------------------------------------------------
/netbox_acls/models/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 | """
3 | Import each of the directory's scripts.
4 | """
5 |
6 | from .access_list_rules import *
7 | from .access_lists import *
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG NETBOX_VARIANT=v4.4
2 |
3 | FROM netboxcommunity/netbox:${NETBOX_VARIANT}
4 |
5 | RUN mkdir -pv /plugins/netbox-acls
6 | COPY . /plugins/netbox-acls
7 |
8 | RUN /usr/local/bin/uv pip install --editable /plugins/netbox-acls
9 |
--------------------------------------------------------------------------------
/.devcontainer/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | autoflake
2 | autopep8
3 | bandit
4 | black
5 | coverage
6 | flake8
7 | isort
8 | mypy
9 | pre-commit
10 | pycodestyle
11 | pydocstyle
12 | pylint
13 | pylint-django
14 | ruff
15 | sourcery-analytics
16 | wily
17 | yapf
18 |
--------------------------------------------------------------------------------
/netbox_acls/graphql/filters/__init__.py:
--------------------------------------------------------------------------------
1 | from .access_list_rules import ACLExtendedRuleFilter, ACLStandardRuleFilter
2 | from .access_lists import AccessListFilter, ACLInterfaceAssignmentFilter
3 |
4 | __all__ = (
5 | "AccessListFilter",
6 | "ACLExtendedRuleFilter",
7 | "ACLInterfaceAssignmentFilter",
8 | "ACLStandardRuleFilter",
9 | )
10 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.override.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: '3.4'
3 | services:
4 | netbox:
5 | build:
6 | dockerfile: Dockerfile-plugin_dev
7 | context: .
8 | ports:
9 | - "8000:8080"
10 | volumes:
11 | - ../:/opt/netbox/netbox/netbox-acls
12 | - ~/.gitconfig:/home/vscode/.gitconfig:z,ro
13 | - ~/.ssh:/home/vscode/.ssh
14 |
--------------------------------------------------------------------------------
/configuration/plugins.py:
--------------------------------------------------------------------------------
1 | # Add your plugins and plugin settings here.
2 | # Of course uncomment this file out.
3 |
4 | # To learn how to build images with your required plugins
5 | # See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins
6 |
7 | PLUGINS = [
8 | "netbox_acls",
9 | ]
10 |
11 | PLUGINS_CONFIG = { # type: ignore
12 | "netbox_acls": {},
13 | }
14 |
--------------------------------------------------------------------------------
/netbox_acls/forms/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 | """
3 | Import each of the directory's scripts.
4 | """
5 |
6 | # from .bulk_create import *
7 | # from .bulk_edit import *
8 |
9 | # from .bulk_import import *
10 | # from .connections import *
11 | from .filtersets import *
12 |
13 | # from .formsets import *
14 | from .models import *
15 |
16 | # from .object_create import *
17 | # from .object_import import *
18 |
--------------------------------------------------------------------------------
/netbox_acls/constants.py:
--------------------------------------------------------------------------------
1 | """
2 | Constants for filters
3 | """
4 | from django.db.models import Q
5 |
6 | ACL_HOST_ASSIGNMENT_MODELS = Q(
7 | Q(app_label="dcim", model="device")
8 | | Q(app_label="dcim", model="virtualchassis")
9 | | Q(app_label="virtualization", model="virtualmachine"),
10 | )
11 |
12 | ACL_INTERFACE_ASSIGNMENT_MODELS = Q(
13 | Q(app_label="dcim", model="interface") | Q(app_label="virtualization", model="vminterface"),
14 | )
15 |
--------------------------------------------------------------------------------
/configuration/logging.py:
--------------------------------------------------------------------------------
1 | # Remove first comment(#) on each line to implement this working logging example.
2 | # Add LOGLEVEL environment variable to netbox if you use this example & want a different log level.
3 | from os import environ
4 |
5 | # Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO.
6 | LOGLEVEL = environ.get("LOGLEVEL", "INFO")
7 |
8 | LOGGING = {
9 | "version": 1,
10 | "disable_existing_loggers": True,
11 | }
12 |
--------------------------------------------------------------------------------
/netbox_acls/tests/api/test_app.py:
--------------------------------------------------------------------------------
1 | from django.urls import reverse
2 | from rest_framework import status
3 | from utilities.testing import APITestCase
4 |
5 |
6 | class AppTest(APITestCase):
7 | def test_root(self):
8 | """Test the API root view."""
9 | url = reverse("plugins-api:netbox_acls-api:api-root")
10 | response = self.client.get(f"{url}?format=api", **self.header)
11 |
12 | self.assertEqual(response.status_code, status.HTTP_200_OK)
13 |
--------------------------------------------------------------------------------
/.devcontainer/configuration/plugins.py:
--------------------------------------------------------------------------------
1 | # Add your plugins and plugin settings here.
2 | # Of course uncomment this file out.
3 |
4 | # To learn how to build images with your required plugins
5 | # See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins
6 |
7 | PLUGINS = [
8 | "netbox_initializers", # Loads demo data
9 | "netbox_acls",
10 | ]
11 |
12 | PLUGINS_CONFIG = { # type: ignore
13 | "netbox_initializers": {},
14 | "netbox_acls": {},
15 | }
16 |
--------------------------------------------------------------------------------
/netbox_acls/api/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | Creates API endpoint URLs for the plugin.
3 | """
4 |
5 | from netbox.api.routers import NetBoxRouter
6 |
7 | from . import views
8 |
9 | app_name = "netbox_acls"
10 |
11 | router = NetBoxRouter()
12 | router.register("access-lists", views.AccessListViewSet)
13 | router.register("interface-assignments", views.ACLInterfaceAssignmentViewSet)
14 | router.register("standard-acl-rules", views.ACLStandardRuleViewSet)
15 | router.register("extended-acl-rules", views.ACLExtendedRuleViewSet)
16 |
17 | urlpatterns = router.urls
18 |
--------------------------------------------------------------------------------
/netbox_acls/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Define the NetBox Plugin
3 | """
4 |
5 | import importlib.metadata
6 |
7 | from netbox.plugins import PluginConfig
8 |
9 |
10 | class NetBoxACLsConfig(PluginConfig):
11 | """
12 | Plugin specifc configuration
13 | """
14 |
15 | name = "netbox_acls"
16 | verbose_name = "Access Lists"
17 | version = importlib.metadata.version("netbox-acls")
18 | description = "Manage simple ACLs in NetBox"
19 | base_url = "access-lists"
20 | min_version = "4.3.0"
21 | max_version = "4.4.99"
22 |
23 |
24 | config = NetBoxACLsConfig
25 |
--------------------------------------------------------------------------------
/.github/workflows/greetings.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Greetings
3 |
4 | on: [pull_request_target, issues]
5 |
6 | jobs:
7 | greeting:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | issues: write
11 | pull-requests: write
12 | steps:
13 | - uses: actions/first-interaction@v1
14 | with:
15 | repo-token: ${{ secrets.GITHUB_TOKEN }}
16 | issue-message: "Thanks for opening this Issue! We really appreciate the feedback & testing from users like you!"
17 | pr-message: "🎉 Thanks for opening this pull request! We really appreciate contributors like you! 🙌"
18 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | - TODO: ACL Form Bubble/ICON Extended/Standard
2 | - TODO: Add an Access List to an Interface Custom Fields after comments - DONE
3 | - TODO: ACL rules, look at last number and increment to next 10
4 | - TODO: Clone for ACL Interface should include device
5 | - TODO: Inconsistent errors for add/edit (where model is using a generic page)
6 | - TODO: Check Constants across codebase for consistency.
7 | - TODO: Test API, Forms, & Models - https://github.com/k01ek/netbox-bgp/tree/main/netbox_bgp/tests , https://github.com/DanSheps/netbox-secretstore/tree/develop/netbox_secretstore/tests & https://github.com/FlxPeters/netbox-plugin-prometheus-sd/tree/main/netbox_prometheus_sd/tests
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # To get started with Dependabot version updates, you'll need to specify which
3 | # package ecosystems to update and where the package manifests are located.
4 | # Please see the documentation for all configuration options:
5 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
6 |
7 | version: 2
8 | updates:
9 | - package-ecosystem: "pip" # See documentation for possible values
10 | directory: "/" # Location of package manifests
11 | schedule:
12 | interval: "weekly"
13 | - package-ecosystem: "github-actions"
14 | directory: "/"
15 | schedule:
16 | interval: "weekly"
17 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | services:
4 | netbox:
5 | build:
6 | dockerfile: Dockerfile
7 | context: .
8 | depends_on:
9 | - postgres
10 | - redis
11 | env_file: env/netbox.env
12 | volumes:
13 | - ./configuration:/etc/netbox/config:z,ro
14 |
15 | # postgres
16 | postgres:
17 | image: postgres:16-alpine
18 | env_file: env/postgres.env
19 |
20 | # redis
21 | redis:
22 | image: redis:7-alpine
23 | command:
24 | - sh
25 | - -c # this is to evaluate the $REDIS_PASSWORD from the env
26 | - redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
27 | env_file: env/redis.env
28 |
--------------------------------------------------------------------------------
/env/netbox.env:
--------------------------------------------------------------------------------
1 | ALLOWED_HOSTS=*
2 | CORS_ORIGIN_ALLOW_ALL=true
3 | DB_HOST=postgres
4 | DB_NAME=netbox
5 | DB_PASSWORD=J5brHrAXFLQSif0K
6 | DB_USER=netbox
7 | DEBUG=true
8 | ENFORCE_GLOBAL_UNIQUE=true
9 | LOGIN_REQUIRED=false
10 | GRAPHQL_ENABLED=true
11 | MAX_PAGE_SIZE=1000
12 | MEDIA_ROOT=/opt/netbox/netbox/media
13 | REDIS_DATABASE=0
14 | REDIS_HOST=redis
15 | REDIS_INSECURE_SKIP_TLS_VERIFY=false
16 | REDIS_PASSWORD=H733Kdjndks81
17 | SECRET_KEY='r(m)9nLGnz$(_q3N4z1k(EFsMCjjjzx08x9VhNVcfd%6RF#r!6DE@+V5Zk2X'
18 | SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
19 | SUPERUSER_EMAIL=admin@example.com
20 | SUPERUSER_NAME=admin
21 | SUPERUSER_PASSWORD=admin
22 | STARTUP_SCRIPTS=false
23 | WEBHOOKS_ENABLED=true
24 |
--------------------------------------------------------------------------------
/.devcontainer/env/netbox.env:
--------------------------------------------------------------------------------
1 | ALLOWED_HOSTS=*
2 | CORS_ORIGIN_ALLOW_ALL=true
3 | DB_HOST=postgres
4 | DB_NAME=netbox
5 | DB_PASSWORD=J5brHrAXFLQSif0K
6 | DB_USER=netbox
7 | DEBUG=true
8 | DEVELOPER_MODE=true
9 | ENFORCE_GLOBAL_UNIQUE=true
10 | LOGIN_REQUIRED=false
11 | GRAPHQL_ENABLED=true
12 | MAX_PAGE_SIZE=1000
13 | MEDIA_ROOT=/opt/netbox/netbox/media
14 | REDIS_DATABASE=0
15 | REDIS_HOST=redis
16 | REDIS_INSECURE_SKIP_TLS_VERIFY=false
17 | REDIS_PASSWORD=H733Kdjndks81
18 | SECRET_KEY='r(m)9nLGnz$(_q3N4z1k(EFsMCjjjzx08x9VhNVcfd%6RF#r!6DE@+V5Zk2X'
19 | SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
20 | SUPERUSER_EMAIL=admin@example.com
21 | SUPERUSER_NAME=admin
22 | SUPERUSER_PASSWORD=admin
23 | WEBHOOKS_ENABLED=true
24 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Runs the NetBox plugin unit tests
3 |
4 | # exit when a command exits with an exit code != 0
5 | set -e
6 |
7 | # The docker compose command to use
8 | doco="docker compose --file docker-compose.yml"
9 |
10 | test_netbox_unit_tests() {
11 | echo "⏱ Running NetBox Unit Tests"
12 | $doco run --rm netbox python manage.py makemigrations netbox_acls --check
13 | $doco run --rm netbox python manage.py test netbox_acls -v 2
14 | }
15 |
16 | test_cleanup() {
17 | echo "💣 Cleaning Up"
18 | $doco down -v
19 | $doco rm -fsv
20 | docker image rm docker.io/library/netbox-acls-netbox || echo ''
21 | }
22 |
23 | echo "🐳🐳🐳 Start testing"
24 |
25 | # Make sure the cleanup script is executed
26 | trap test_cleanup EXIT ERR
27 | test_netbox_unit_tests
28 |
29 | echo "🐳🐳🐳 Done testing"
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # Reference: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
3 | blank_issues_enabled: false
4 | contact_links:
5 | #- name: 📕 Plugin Documentation
6 | # url: https://netbox-acls.readthedocs.io
7 | # about: "Please refer to the documentation before raising a bug or feature request."
8 | - name: 📖 Contributing Policy
9 | url: https://github.com/ryanmerolle/netbox-acls/blob/dev/CONTRIBUTING.md
10 | about: "Please read through our contributing policy before opening an issue or pull request"
11 | - name: 💬 Community Slack
12 | url: https://netdev.chat/
13 | about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems"
14 |
--------------------------------------------------------------------------------
/netbox_acls/migrations/0004_netbox_acls.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-06-23 21:02
2 |
3 | import django.core.validators
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("netbox_acls", "0003_netbox_acls"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="accesslist",
15 | name="name",
16 | field=models.CharField(
17 | max_length=500,
18 | validators=[
19 | django.core.validators.RegexValidator(
20 | "^[a-zA-Z0-9-_]+$", "Only alphanumeric, hyphens, and underscores characters are allowed."
21 | )
22 | ],
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/netbox_acls/graphql/enums.py:
--------------------------------------------------------------------------------
1 | import strawberry
2 |
3 | from ..choices import (
4 | ACLActionChoices,
5 | ACLAssignmentDirectionChoices,
6 | ACLProtocolChoices,
7 | ACLRuleActionChoices,
8 | ACLTypeChoices,
9 | )
10 |
11 | __all__ = (
12 | "ACLActionEnum",
13 | "ACLAssignmentDirectionEnum",
14 | "ACLProtocolEnum",
15 | "ACLRuleActionEnum",
16 | "ACLTypeEnum",
17 | )
18 |
19 | #
20 | # Access List
21 | #
22 |
23 | ACLActionEnum = strawberry.enum(ACLActionChoices.as_enum())
24 | ACLTypeEnum = strawberry.enum(ACLTypeChoices.as_enum())
25 |
26 | #
27 | # Access List Assignments
28 | #
29 |
30 | ACLAssignmentDirectionEnum = strawberry.enum(ACLAssignmentDirectionChoices.as_enum())
31 |
32 | #
33 | # Access List Rules
34 | #
35 |
36 | ACLProtocolEnum = strawberry.enum(ACLProtocolChoices.as_enum())
37 | ACLRuleActionEnum = strawberry.enum(ACLRuleActionChoices.as_enum())
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/housekeeping.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🏡 Housekeeping
3 | description: A change pertaining to the codebase itself (developers only)
4 | title: "[Housekeeping]: "
5 | labels: ["housekeeping"]
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: >
10 | **NOTE:** This template is for use by maintainers only. Please do not submit
11 | an issue using this template unless you have been specifically asked to do so.
12 | - type: textarea
13 | attributes:
14 | label: Proposed Changes
15 | description: >
16 | Describe in detail the new feature or behavior you'd like to propose.
17 | Include any specific changes to work flows, data models, or the user interface.
18 | validations:
19 | required: true
20 | - type: textarea
21 | attributes:
22 | label: Justification
23 | description: Please provide justification for the proposed change(s).
24 | validations:
25 | required: true
26 |
--------------------------------------------------------------------------------
/netbox_acls/templates/inc/view_tab.html:
--------------------------------------------------------------------------------
1 | {% extends 'generic/object.html' %}
2 | {% load buttons %}
3 | {% load helpers %}
4 | {% load plugins %}
5 | {% load render_table from django_tables2 %}
6 |
7 | {% block extra_controls %}
8 | {% if perms.netbox_todo.add_accesslist %}
9 |
10 | Add Access List
11 |
12 | {% endif %}
13 | {% endblock extra_controls %}
14 |
15 | {% block content %}
16 | {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
17 |
25 | {% endblock content %}
26 |
27 | {% block modals %}
28 | {{ block.super }}
29 | {% table_config_form table %}
30 | {% endblock modals %}
31 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # Dependency Review Action
3 | #
4 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
5 | #
6 | # Source repository: https://github.com/actions/dependency-review-action
7 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
8 | name: 'Dependency Review'
9 | on: [pull_request]
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | dependency-review:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: 'Checkout Repository'
19 | uses: actions/checkout@v4
20 | - name: 'Dependency Review'
21 | uses: actions/dependency-review-action@v4
22 |
--------------------------------------------------------------------------------
/.devcontainer/entrypoint-dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | USER=vscode
4 |
5 | # Reconfigure User id if set by user
6 | if [ ! -z "${USER_UID}" ] && [ "${USER_UID}" != "`id -u ${USER}`" ] ; then
7 | echo -n "Update uid for user ${USER} with ${USER_UID}"
8 | usermod -u ${USER_UID} ${USER}
9 | echo "... updated"
10 | else
11 | echo "skipping UID configuration"
12 | fi
13 |
14 | if [ -n "${USER_GID}" ] && [ "${USER_GID}" != "`id -g ${USER}`" ] ; then
15 | echo -n "Update gid for group ${USER} with ${USER_GID}"
16 | usermod -u ${USER_UID} ${USER}
17 | echo "... updated"
18 | else
19 | echo "skipping GID configuration"
20 | fi
21 |
22 | #if [ -z "$SSH_AUTH_SOCK" ]; then
23 | # # Check for a currently running instance of the agent
24 | # RUNNING_AGENT="`ps -ax | grep 'ssh-agent -s' | grep -v grep | wc -l | tr -d '[:space:]'`"
25 | # if [ "$RUNNING_AGENT" = "0" ]; then
26 | # # Launch a new instance of the agent
27 | # ssh-agent -s &> $HOME/.ssh/ssh-agent
28 | # fi
29 | # eval `cat $HOME/.ssh/ssh-agent`
30 | #fi
31 |
32 | /bin/sh -c "while sleep 1000; do :; done"
33 |
--------------------------------------------------------------------------------
/netbox_acls/graphql/schema.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import strawberry
4 | import strawberry_django
5 |
6 | from .types import (
7 | AccessListType,
8 | ACLExtendedRuleType,
9 | ACLInterfaceAssignmentType,
10 | ACLStandardRuleType,
11 | )
12 |
13 |
14 | @strawberry.type(name="Query")
15 | class NetBoxACLSQuery:
16 | """
17 | Defines the queries available to this plugin via the graphql api.
18 | """
19 |
20 | access_list: AccessListType = strawberry_django.field()
21 | access_list_list: List[AccessListType] = strawberry_django.field()
22 |
23 | acl_extended_rule: ACLExtendedRuleType = strawberry_django.field()
24 | acl_extended_rule_list: List[ACLExtendedRuleType] = strawberry_django.field()
25 |
26 | acl_standard_rule: ACLStandardRuleType = strawberry_django.field()
27 | acl_standard_rule_list: List[ACLStandardRuleType] = strawberry_django.field()
28 |
29 | acl_interface_assignment: ACLInterfaceAssignmentType = strawberry_django.field()
30 | acl_interface_assignment_list: List[ACLInterfaceAssignmentType] = strawberry_django.field()
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation_change.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: 📖 Documentation Change
3 | description: Suggest an addition or modification to the NetBox Access Lists plugin documentation
4 | title: "[Docs]: "
5 | labels: ["documentation"]
6 | body:
7 | - type: dropdown
8 | attributes:
9 | label: Change Type
10 | description: What type of change are you proposing?
11 | options:
12 | - Addition
13 | - Correction
14 | - Removal
15 | - Cleanup (formatting, typos, etc.)
16 | validations:
17 | required: true
18 | - type: dropdown
19 | attributes:
20 | label: Area
21 | description: To what section of the documentation does this change primarily pertain?
22 | options:
23 | - Installation instructions
24 | - Configuration parameters
25 | - Functionality/features
26 | - Administration/development
27 | - Other
28 | validations:
29 | required: true
30 | - type: textarea
31 | attributes:
32 | label: Proposed Changes
33 | description: Describe the proposed changes and why they are necessary.
34 | validations:
35 | required: true
36 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # This workflow will upload a Python Package using Twine when a release is created
3 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
4 |
5 | # This workflow uses actions that are not certified by GitHub.
6 | # They are provided by a third-party and are governed by
7 | # separate terms of service, privacy policy, and support
8 | # documentation.
9 |
10 | name: Upload Python Package
11 |
12 | on:
13 | release:
14 | types: [published]
15 |
16 | permissions:
17 | contents: read
18 |
19 | jobs:
20 | deploy:
21 |
22 | runs-on: ubuntu-latest
23 |
24 | steps:
25 | - uses: actions/checkout@v4
26 | - name: Set up Python
27 | uses: actions/setup-python@v5
28 | with:
29 | python-version: '3.x'
30 | - name: Install dependencies
31 | run: |
32 | python -m pip install --upgrade pip
33 | pip install build
34 | - name: Build package
35 | run: python -m build
36 | - name: Publish package
37 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
38 | with:
39 | user: __token__
40 | password: ${{ secrets.PYPI_API_TOKEN }}
41 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: CI
3 |
4 | on:
5 | push:
6 | pull_request:
7 |
8 | # This ensures that previous jobs for the workflow are canceled when the ref is
9 | # updated.
10 | concurrency:
11 | group: ${{ github.workflow }}-${{ github.ref }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | run-lint:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 | with:
21 | # Full git history is needed to get a proper list of changed files within `super-linter`
22 | fetch-depth: 0
23 |
24 | - name: Lint Code Base
25 | uses: github/super-linter/slim@v7
26 | env:
27 | DEFAULT_BRANCH: dev
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | SUPPRESS_POSSUM: true
30 | LINTER_RULES_PATH: /
31 | VALIDATE_ALL_CODEBASE: false
32 | VALIDATE_DOCKERFILE: false
33 | VALIDATE_JSCPD: true
34 | FILTER_REGEX_EXCLUDE: (.*/)?(configuration/.*)
35 |
36 | test:
37 | runs-on: ubuntu-latest
38 | name: Runs plugin tests
39 | needs: run-lint
40 | steps:
41 | - id: git-checkout
42 | name: Checkout
43 | uses: actions/checkout@v4
44 |
45 | - id: docker-test
46 | name: Test the image
47 | run: ./test.sh
48 |
--------------------------------------------------------------------------------
/netbox_acls/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | Map Views to URLs.
3 | """
4 |
5 | from django.urls import include, path
6 | from utilities.urls import get_model_urls
7 |
8 | from . import views # noqa F401
9 |
10 | urlpatterns = (
11 | # Access Lists
12 | path(
13 | "access-lists/",
14 | include(get_model_urls("netbox_acls", "accesslist", detail=False)),
15 | ),
16 | path(
17 | "access-lists//",
18 | include(get_model_urls("netbox_acls", "accesslist")),
19 | ),
20 | # Access List Interface Assignments
21 | path(
22 | "interface-assignments/",
23 | include(get_model_urls("netbox_acls", "aclinterfaceassignment", detail=False)),
24 | ),
25 | path(
26 | "interface-assignments//",
27 | include(get_model_urls("netbox_acls", "aclinterfaceassignment")),
28 | ),
29 | # Standard Access List Rules
30 | path(
31 | "standard-rules/",
32 | include(get_model_urls("netbox_acls", "aclstandardrule", detail=False)),
33 | ),
34 | path(
35 | "standard-rules//",
36 | include(get_model_urls("netbox_acls", "aclstandardrule")),
37 | ),
38 | # Extended Access List Rules
39 | path(
40 | "extended-rules/",
41 | include(get_model_urls("netbox_acls", "aclextendedrule", detail=False)),
42 | ),
43 | path(
44 | "extended-rules//",
45 | include(get_model_urls("netbox_acls", "aclextendedrule")),
46 | ),
47 | )
48 |
--------------------------------------------------------------------------------
/ruff.toml:
--------------------------------------------------------------------------------
1 | # Ruff configuration
2 | ####################
3 |
4 | # Exclude a variety of commonly ignored directories.
5 | exclude = [
6 | ".bzr",
7 | ".direnv",
8 | ".eggs",
9 | ".git",
10 | ".git-rewrite",
11 | ".hg",
12 | ".ipynb_checkpoints",
13 | ".mypy_cache",
14 | ".nox",
15 | ".pants.d",
16 | ".pyenv",
17 | ".pytest_cache",
18 | ".pytype",
19 | ".ruff_cache",
20 | ".svn",
21 | ".tox",
22 | ".venv",
23 | ".vscode",
24 | "__pypackages__",
25 | "_build",
26 | "buck-out",
27 | "build",
28 | "dist",
29 | "node_modules",
30 | "site-packages",
31 | "venv",
32 | ]
33 |
34 | # Enforce line length and indent-width
35 | line-length = 120
36 | indent-width = 4
37 |
38 | # Always generate Python 3.10-compatible code.
39 | target-version = "py310"
40 |
41 | [lint]
42 | # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
43 | select = ["E4", "E7", "E9", "F"]
44 | ignore = []
45 |
46 | # Allow fix for all enabled rules (when `--fix`) is provided.
47 | fixable = ["ALL"]
48 | unfixable = []
49 |
50 | # Allow unused variables when underscore-prefixed.
51 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
52 |
53 | [format]
54 | # Like Black, use double quotes for strings.
55 | quote-style = "double"
56 |
57 | # Like Black, indent with spaces, rather than tabs.
58 | indent-style = "space"
59 |
60 | # Like Black, respect magic trailing commas.
61 | skip-magic-trailing-comma = false
62 |
63 | # Enforce UNIX line ending
64 | line-ending = "lf"
65 |
--------------------------------------------------------------------------------
/netbox_acls/migrations/0003_netbox_acls.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.5 on 2023-01-25 20:08
2 |
3 | import utilities.json
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("netbox_acls", "0002_alter_accesslist_options_and_more"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="accesslist",
15 | name="custom_field_data",
16 | field=models.JSONField(
17 | blank=True,
18 | default=dict,
19 | encoder=utilities.json.CustomFieldJSONEncoder,
20 | ),
21 | ),
22 | migrations.AlterField(
23 | model_name="aclextendedrule",
24 | name="custom_field_data",
25 | field=models.JSONField(
26 | blank=True,
27 | default=dict,
28 | encoder=utilities.json.CustomFieldJSONEncoder,
29 | ),
30 | ),
31 | migrations.AlterField(
32 | model_name="aclinterfaceassignment",
33 | name="custom_field_data",
34 | field=models.JSONField(
35 | blank=True,
36 | default=dict,
37 | encoder=utilities.json.CustomFieldJSONEncoder,
38 | ),
39 | ),
40 | migrations.AlterField(
41 | model_name="aclstandardrule",
42 | name="custom_field_data",
43 | field=models.JSONField(
44 | blank=True,
45 | default=dict,
46 | encoder=utilities.json.CustomFieldJSONEncoder,
47 | ),
48 | ),
49 | ]
50 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.setuptools]
6 | packages = ["netbox_acls"]
7 | package-data = {"netbox_acls" = ["**/*", "templates/**"]}
8 |
9 |
10 | [tool.black]
11 | line-length = 140
12 |
13 | [tool.isort]
14 | profile = "black"
15 | include_trailing_comma = true
16 | multi_line_output = 3
17 |
18 | [tool.pylint]
19 | max-line-length = 140
20 |
21 | [tool.pyright]
22 | include = ["netbox_acls"]
23 | exclude = [
24 | "**/node_modules",
25 | "**/__pycache__",
26 | ]
27 | reportMissingImports = true
28 | reportMissingTypeStubs = false
29 |
30 | [tool.ruff]
31 | line-length = 140
32 |
33 | [project]
34 | name = "netbox-acls"
35 | version = "1.9.1"
36 | readme = "README.md"
37 | requires-python = ">=3.10.0"
38 | classifiers=[
39 | 'Development Status :: 5 - Production/Stable',
40 | 'Intended Audience :: Developers',
41 | 'Natural Language :: English',
42 | "Programming Language :: Python :: 3 :: Only",
43 | 'Programming Language :: Python :: 3.10',
44 | 'Programming Language :: Python :: 3.11',
45 | 'Programming Language :: Python :: 3.12',
46 | 'Intended Audience :: System Administrators',
47 | 'Intended Audience :: Telecommunications Industry',
48 | 'Framework :: Django',
49 | 'Topic :: System :: Networking',
50 | 'Topic :: Internet',
51 | ]
52 | keywords = ["netbox", "netbox-plugin"]
53 | license = {file = "LICENSE.txt"}
54 |
55 |
56 | [project.urls]
57 | Documentation = "https://github.com/netbox-community/netbox-acls/blob/dev/README.md"
58 | Source = "https://github.com/netbox-community/netbox-acls/"
59 | Tracker = "https://github.com/netbox-community/netbox-acls/issues"
60 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile-plugin_dev:
--------------------------------------------------------------------------------
1 | ARG NETBOX_VARIANT=v3.7
2 |
3 | FROM netboxcommunity/netbox:${NETBOX_VARIANT}
4 |
5 | ARG NETBOX_INITIALIZERS_VARIANT=3.7.*
6 |
7 | ARG DEBIAN_FRONTEND=noninteractive
8 |
9 | # Install APT packages
10 | # hadolint ignore=DL3008
11 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
12 | && apt-get -y install --no-install-recommends curl git make openssh-client python3.11-dev sudo wget zsh \
13 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*
14 |
15 | # Install development & ide dependencies
16 | COPY requirements-dev.txt /tmp/pip-tmp/
17 | RUN /opt/netbox/venv/bin/python3 -m pip install --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements-dev.txt \
18 | && rm -rf /tmp/*
19 |
20 | ARG USERNAME=ubuntu
21 | ARG USER_UID=1000
22 | ARG USER_GID=$USER_UID
23 |
24 | RUN usermod -aG sudo $USERNAME \
25 | && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \
26 | && mkdir /opt/netbox/netbox/netbox-acls \
27 | && chown $USERNAME:$USERNAME /opt/netbox /etc/netbox /opt/unit -R
28 |
29 | USER $USERNAME
30 |
31 | SHELL ["/bin/bash", "-o", "pipefail", "-c"]
32 | # Add oh my zsh
33 | RUN wget --quiet https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true
34 |
35 | COPY .bashrc /home/vscode/.bashrc
36 | COPY .zshrc /home/vscode/.zshrc
37 |
38 | RUN /opt/netbox/venv/bin/pip install --no-warn-script-location netbox-initializers==${NETBOX_INITIALIZERS_VARIANT}
39 |
40 | WORKDIR /opt/netbox/netbox/netbox-acls
41 |
42 | # hadolint ignore=DL3002
43 | USER root
44 |
45 | COPY entrypoint-dev.sh /bin/entrypoint-dev.sh
46 | RUN chmod +x /bin/entrypoint-dev.sh
47 |
48 | CMD ["/bin/entrypoint-dev.sh"]
49 |
--------------------------------------------------------------------------------
/.devcontainer/configuration/logging.py:
--------------------------------------------------------------------------------
1 | # Remove first comment(#) on each line to implement this working logging example.
2 | # Add LOGLEVEL environment variable to netbox if you use this example & want a different log level.
3 | from os import environ
4 |
5 | # Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO.
6 | LOGLEVEL = environ.get("LOGLEVEL", "INFO")
7 |
8 | LOGGING = {
9 | "version": 1,
10 | "disable_existing_loggers": False,
11 | "formatters": {
12 | "verbose": {
13 | "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
14 | "style": "{",
15 | },
16 | "simple": {
17 | "format": "{levelname} {message}",
18 | "style": "{",
19 | },
20 | },
21 | "filters": {
22 | "require_debug_false": {
23 | "()": "django.utils.log.RequireDebugFalse",
24 | },
25 | },
26 | "handlers": {
27 | "console": {
28 | "level": LOGLEVEL,
29 | "filters": ["require_debug_false"],
30 | "class": "logging.StreamHandler",
31 | "formatter": "simple",
32 | },
33 | "mail_admins": {
34 | "level": "ERROR",
35 | "class": "django.utils.log.AdminEmailHandler",
36 | "filters": ["require_debug_false"],
37 | },
38 | },
39 | "loggers": {
40 | "django": {
41 | "handlers": ["console"],
42 | "propagate": True,
43 | },
44 | "django.request": {
45 | "handlers": ["mail_admins"],
46 | "level": "ERROR",
47 | "propagate": False,
48 | },
49 | "django_auth_ldap": {
50 | "handlers": ["console"],
51 | "level": LOGLEVEL,
52 | },
53 | },
54 | }
55 |
--------------------------------------------------------------------------------
/netbox_acls/templates/netbox_acls/aclinterfaceassignment.html:
--------------------------------------------------------------------------------
1 | {% extends 'generic/object.html' %}
2 | {% load render_table from django_tables2 %}
3 |
4 | {% block breadcrumbs %}
5 | {{ block.super }}
6 | {{ object.access_list }}
7 | {% endblock breadcrumbs %}
8 |
9 | {% block content %}
10 |
11 |
12 |
13 |
14 |
15 |
16 | | Host |
17 |
18 | {% if object.assigned_object.device %}
19 | {{ object.assigned_object.device|linkify|placeholder }}
20 | {% else %}
21 | {{ object.assigned_object.virtual_machine|linkify|placeholder }}
22 | {% endif %}
23 | |
24 |
25 |
26 | | Interface |
27 | {{ object.assigned_object|linkify|placeholder }} |
28 |
29 |
30 | | Access List |
31 | {{ object.access_list|linkify }} |
32 |
33 |
34 | | Direction |
35 | {% badge object.get_direction_display bg_color=object.get_direction_color %} |
36 |
37 |
38 |
39 | {% include 'inc/panels/custom_fields.html' %}
40 |
41 |
42 | {% include 'inc/panels/tags.html' %}
43 | {% include 'inc/panels/comments.html' %}
44 |
45 |
46 | {% endblock content %}
47 |
--------------------------------------------------------------------------------
/netbox_acls/templates/netbox_acls/aclstandardrule.html:
--------------------------------------------------------------------------------
1 | {% extends 'generic/object.html' %}
2 |
3 | {% block breadcrumbs %}
4 | {{ block.super }}
5 | {{ object.access_list }}
6 | {% endblock breadcrumbs %}
7 |
8 | {% block content %}
9 |
10 |
11 |
12 |
13 |
14 |
15 | | Access List |
16 | {{ object.access_list|linkify }} |
17 |
18 |
19 | | Index |
20 | {{ object.index }} |
21 |
22 |
23 | | Description |
24 | {{ object.description|placeholder }} |
25 |
26 |
27 |
28 | {% include 'inc/panels/custom_fields.html' %}
29 | {% include 'inc/panels/tags.html' %}
30 |
31 |
32 |
33 |
34 |
35 |
36 | | Action |
37 | {% badge object.get_action_display bg_color=object.get_action_color %} |
38 |
39 |
40 | | Remark |
41 | {{ object.remark|placeholder }} |
42 |
43 |
44 | | Source Prefix |
45 | {{ object.source_prefix|linkify|placeholder }} |
46 |
47 |
48 |
49 |
50 |
51 | {% endblock content %}
52 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
14 | # Pull Request
15 |
16 | ## Related Issue
17 |
18 |
21 |
22 | ## New Behavior
23 |
24 |
27 |
28 | ...
29 |
30 | ## Contrast to Current Behavior
31 |
32 |
36 |
37 | ...
38 |
39 | ## Discussion: Benefits and Drawbacks
40 |
41 |
53 |
54 | ...
55 |
56 | ## Changes to the Documentation
57 |
58 |
62 |
63 | ...
64 |
65 | ## Proposed Release Note Entry
66 |
67 |
71 |
72 | ...
73 |
74 | ## Double Check
75 |
76 |
79 |
80 | * [ ] I have explained my PR according to the information in the comments
81 | or in a linked issue.
82 | * [ ] My PR targets the `dev` branch.
83 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | description: Report a reproducible bug in the current release of this NetBox Plugin
4 | title: "[Bug]: "
5 | labels: ["bug"]
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: >
10 | **NOTE:** This form is only for reporting _reproducible bugs_ in a current NetBox plugin
11 | installation.
12 |
13 | - Check the release notes:
14 | https://github.com/ryanmerolle/netbox-acls/releases
15 | - Look through the issues already resolved:
16 | https://github.com/ryanmerolle/netbox-acls/issues?q=is%3Aclosed
17 | - Post to Github Discussions if you need setup or usage help that is not a bug:
18 | https://github.com/ryanmerolle/netbox-acls/discussions
19 | - Join the `#netbox` channel on our Slack:
20 | https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ
21 |
22 | - type: input
23 | attributes:
24 | label: NetBox access-list plugin version
25 | description: What version of the NetBox access-list plugin are you currently running?
26 | placeholder: v1.5.0
27 | validations:
28 | required: true
29 | - type: input
30 | attributes:
31 | label: NetBox version
32 | description: What version of NetBox are you currently running?
33 | placeholder: v3.7.4
34 | validations:
35 | required: true
36 | - type: textarea
37 | attributes:
38 | label: Steps to Reproduce
39 | description: >
40 | Describe in detail the exact steps that someone else can take to
41 | reproduce this bug using the current stable release of the NetBox access-list plugin.
42 | #placeholder: |
43 | validations:
44 | required: true
45 | - type: textarea
46 | attributes:
47 | label: Expected Behavior
48 | description: What did you expect to happen?
49 | placeholder: A new widget should have been created with the specified attributes
50 | validations:
51 | required: true
52 | - type: textarea
53 | attributes:
54 | label: Observed Behavior
55 | description: What happened instead?
56 | placeholder: A TypeError exception was raised
57 | validations:
58 | required: true
59 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: ✨ Feature Request
3 | description: Propose a new feature or enhancement
4 | title: "[Feature]: "
5 | labels: ["enhancement"]
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: >
10 | **NOTE:** This form is only for submitting well-formed proposals to extend or modify
11 | NetBox in some way. If you're trying to solve a problem but can't figure out how, or if
12 | you still need time to work on the details of a proposed new feature, please start a
13 | [discussion](https://github.com/ryanmerolle/netbox-acls/discussions) instead.
14 | - type: input
15 | attributes:
16 | label: NetBox version
17 | description: What version of NetBox are you currently running?
18 | placeholder: v3.6.3
19 | validations:
20 | required: true
21 | - type: dropdown
22 | attributes:
23 | label: Feature type
24 | options:
25 | - New Model to plugin
26 | - Change to existing model
27 | - Add a function
28 | - Remove a function
29 | validations:
30 | required: true
31 | - type: textarea
32 | attributes:
33 | label: Proposed functionality
34 | description: >
35 | Describe in detail the new feature or behavior you are proposing. Include any specific changes
36 | to work flows, data models, and/or dependencies. The more detail you provide here, the
37 | greater chance your proposal has of being discussed. Feature requests which don't include an
38 | actionable implementation plan will be rejected.
39 | validations:
40 | required: true
41 | - type: textarea
42 | attributes:
43 | label: Use case
44 | description: >
45 | Explain how adding this functionality would benefit NetBox users & specifically this plugin. What need does it address?
46 | validations:
47 | required: true
48 | - type: textarea
49 | attributes:
50 | label: External dependencies
51 | description: >
52 | List any new dependencies on external libraries or services that this new feature would
53 | introduce. For example, does the proposal require the installation of a new Python package?
54 | (Not all new features introduce new dependencies.)
55 |
--------------------------------------------------------------------------------
/netbox_acls/graphql/filters/access_lists.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Annotated
2 |
3 | import strawberry
4 | import strawberry_django
5 | from core.graphql.filters import ContentTypeFilter
6 | from netbox.graphql.filter_mixins import NetBoxModelFilterMixin
7 | from strawberry.scalars import ID
8 | from strawberry_django import FilterLookup
9 |
10 | from ... import models
11 |
12 | if TYPE_CHECKING:
13 | from ..enums import (
14 | ACLActionEnum,
15 | ACLAssignmentDirectionEnum,
16 | ACLTypeEnum,
17 | )
18 |
19 |
20 | __all__ = (
21 | "AccessListFilter",
22 | "ACLInterfaceAssignmentFilter",
23 | )
24 |
25 |
26 | @strawberry_django.filter(models.AccessList, lookups=True)
27 | class AccessListFilter(NetBoxModelFilterMixin):
28 | """
29 | GraphQL filter definition for the AccessList model.
30 | """
31 |
32 | name: FilterLookup[str] | None = strawberry_django.filter_field()
33 | assigned_object_type: Annotated["ContentTypeFilter", strawberry.lazy("core.graphql.filters")] | None = (
34 | strawberry_django.filter_field()
35 | )
36 | assigned_object_id: ID | None = strawberry_django.filter_field()
37 | type: Annotated["ACLTypeEnum", strawberry.lazy("netbox_acls.graphql.enums")] | None = (
38 | strawberry_django.filter_field()
39 | )
40 | default_action: Annotated["ACLActionEnum", strawberry.lazy("netbox_acls.graphql.enums")] | None = (
41 | strawberry_django.filter_field()
42 | )
43 |
44 |
45 | @strawberry_django.filter(models.ACLInterfaceAssignment, lookups=True)
46 | class ACLInterfaceAssignmentFilter(NetBoxModelFilterMixin):
47 | """
48 | GraphQL filter definition for the ACLInterfaceAssignment model.
49 | """
50 |
51 | access_list: Annotated["AccessListFilter", strawberry.lazy("netbox_acls.graphql.filters")] | None = (
52 | strawberry_django.filter_field()
53 | )
54 | access_list_id: ID | None = strawberry_django.filter_field()
55 | direction: Annotated["ACLAssignmentDirectionEnum", strawberry.lazy("netbox_acls.graphql.enums")] | None = (
56 | strawberry_django.filter_field()
57 | )
58 | assigned_object_type: Annotated["ContentTypeFilter", strawberry.lazy("core.graphql.filters")] | None = (
59 | strawberry_django.filter_field()
60 | )
61 | assigned_object_id: ID | None = strawberry_django.filter_field()
62 |
--------------------------------------------------------------------------------
/netbox_acls/choices.py:
--------------------------------------------------------------------------------
1 | """
2 | Defines the various choices to be used by the models, forms, and other plugin specifics.
3 | """
4 |
5 | from utilities.choices import ChoiceSet
6 |
7 | __all__ = (
8 | "ACLActionChoices",
9 | "ACLAssignmentDirectionChoices",
10 | "ACLProtocolChoices",
11 | "ACLRuleActionChoices",
12 | "ACLTypeChoices",
13 | "ACLProtocolChoices",
14 | )
15 |
16 |
17 | class ACLActionChoices(ChoiceSet):
18 | """
19 | Defines the choices availble for the Access Lists plugin specific to ACL default_action.
20 | """
21 |
22 | ACTION_DENY = "deny"
23 | ACTION_PERMIT = "permit"
24 | ACTION_REJECT = "reject"
25 |
26 | CHOICES = [
27 | (ACTION_DENY, "Deny", "red"),
28 | (ACTION_PERMIT, "Permit", "green"),
29 | (ACTION_REJECT, "Reject (Reset)", "orange"),
30 | ]
31 |
32 |
33 | class ACLRuleActionChoices(ChoiceSet):
34 | """
35 | Defines the choices availble for the Access Lists plugin specific to ACL rule actions.
36 | """
37 |
38 | ACTION_DENY = "deny"
39 | ACTION_PERMIT = "permit"
40 | ACTION_REMARK = "remark"
41 |
42 | CHOICES = [
43 | (ACTION_DENY, "Deny", "red"),
44 | (ACTION_PERMIT, "Permit", "green"),
45 | (ACTION_REMARK, "Remark", "blue"),
46 | ]
47 |
48 |
49 | class ACLAssignmentDirectionChoices(ChoiceSet):
50 | """
51 | Defines the direction of the application of the ACL on an associated interface.
52 | """
53 |
54 | DIRECTION_INGRESS = "ingress"
55 | DIRECTION_EGRESS = "egress"
56 |
57 | CHOICES = [
58 | (DIRECTION_INGRESS, "Ingress", "blue"),
59 | (DIRECTION_EGRESS, "Egress", "purple"),
60 | ]
61 |
62 |
63 | class ACLTypeChoices(ChoiceSet):
64 | """
65 | Defines the choices availble for the Access Lists plugin specific to ACL type.
66 | """
67 |
68 | TYPE_STANDARD = "standard"
69 | TYPE_EXTENDED = "extended"
70 |
71 | CHOICES = [
72 | (TYPE_EXTENDED, "Extended", "purple"),
73 | (TYPE_STANDARD, "Standard", "blue"),
74 | ]
75 |
76 |
77 | class ACLProtocolChoices(ChoiceSet):
78 | """
79 | Defines the choices availble for the Access Lists plugin specific to ACL Rule protocol.
80 | """
81 |
82 | PROTOCOL_ICMP = "icmp"
83 | PROTOCOL_TCP = "tcp"
84 | PROTOCOL_UDP = "udp"
85 |
86 | CHOICES = [
87 | (PROTOCOL_ICMP, "ICMP", "purple"),
88 | (PROTOCOL_TCP, "TCP", "blue"),
89 | (PROTOCOL_UDP, "UDP", "orange"),
90 | ]
91 |
--------------------------------------------------------------------------------
/netbox_acls/templates/netbox_acls/aclextendedrule.html:
--------------------------------------------------------------------------------
1 | {% extends 'generic/object.html' %}
2 |
3 | {% block breadcrumbs %}
4 | {{ block.super }}
5 | {{ object.access_list }}
6 | {% endblock breadcrumbs %}
7 |
8 | {% block content %}
9 |
10 |
11 |
12 |
13 |
14 |
15 | | Access List |
16 | {{ object.access_list|linkify }} |
17 |
18 |
19 | | Description |
20 | {{ object.description|placeholder }} |
21 |
22 |
23 | | Index |
24 | {{ object.index }} |
25 |
26 |
27 |
28 | {% include 'inc/panels/custom_fields.html' %}
29 | {% include 'inc/panels/tags.html' %}
30 |
31 |
32 |
33 |
34 |
35 |
36 | | Action |
37 | {% badge object.get_action_display bg_color=object.get_action_color %} |
38 |
39 |
40 | | Remark |
41 | {{ object.remark|placeholder }} |
42 |
43 |
44 | | Protocol |
45 | {{ object.get_protocol_display|placeholder }} |
46 |
47 |
48 | | Source Prefix |
49 | {{ object.source_prefix|linkify|placeholder }} |
50 |
51 |
52 | | Source Ports |
53 | {{ object.source_ports|join:", "|placeholder }} |
54 |
55 |
56 | | Destination Prefix |
57 | {{ object.destination_prefix|linkify|placeholder }} |
58 |
59 |
60 | | Destination Ports |
61 | {{ object.destination_ports|join:", "|placeholder }} |
62 |
63 |
64 |
65 |
66 |
67 | {% endblock content %}
68 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: '3.4'
3 | services:
4 | netbox: &netbox
5 | image: netboxcommunity/netbox:${VARIANT-latest}
6 | depends_on:
7 | - postgres
8 | - redis
9 | #- redis-cache
10 | #- netbox-worker
11 | env_file: env/netbox.env
12 | user: 'unit:root'
13 | healthcheck:
14 | start_period: 60s
15 | timeout: 3s
16 | interval: 15s
17 | test: "curl -f http://localhost:8080/api/ || exit 1"
18 | volumes:
19 | - ./configuration:/etc/netbox/config:ro
20 | #- netbox-media-files:/opt/netbox/netbox/media:rw
21 | #- netbox-reports-files:/opt/netbox/netbox/reports:rw
22 | #- netbox-scripts-files:/opt/netbox/netbox/scripts:rw
23 | #netbox-worker:
24 | # <<: *netbox
25 | # depends_on:
26 | # netbox:
27 | # condition: service_healthy
28 | # command:
29 | # - /opt/netbox/venv/bin/python
30 | # - /opt/netbox/netbox/manage.py
31 | # - rqworker
32 | # healthcheck:
33 | # start_period: 20s
34 | # timeout: 3s
35 | # interval: 15s
36 | # test: "ps -aux | grep -v grep | grep -q rqworker || exit 1"
37 | #netbox-housekeeping:
38 | # <<: *netbox
39 | # depends_on:
40 | # netbox:
41 | # condition: service_healthy
42 | # command:
43 | # - /opt/netbox/housekeeping.sh
44 | # healthcheck:
45 | # start_period: 20s
46 | # timeout: 3s
47 | # interval: 15s
48 | # test: "ps -aux | grep -v grep | grep -q housekeeping || exit 1"
49 |
50 | # postgres
51 | postgres:
52 | image: postgres:16-alpine
53 | env_file: env/postgres.env
54 | volumes:
55 | - netbox-postgres-data:/var/lib/postgresql/data
56 |
57 | # redis
58 | redis:
59 | image: redis:7-alpine
60 | command:
61 | - sh
62 | - -c # this is to evaluate the $REDIS_PASSWORD from the env
63 | - redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
64 | env_file: env/redis.env
65 | #volumes:
66 | # - netbox-redis-data:/data
67 | #redis-cache:
68 | # image: redis:7-alpine
69 | # command:
70 | # - sh
71 | # - -c # this is to evaluate the $REDIS_PASSWORD from the env
72 | # - redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
73 | # env_file: env/redis-cache.env
74 | # volumes:
75 | # - netbox-redis-cache-data:/data
76 |
77 | volumes:
78 | #netbox-media-files:
79 | # driver: local
80 | netbox-postgres-data:
81 | driver: local
82 | #netbox-redis-data:
83 | # driver: local
84 | #netbox-redis-cache-data:
85 | # driver: local
86 |
--------------------------------------------------------------------------------
/netbox_acls/api/views.py:
--------------------------------------------------------------------------------
1 | """
2 | Create views to handle the API logic.
3 | A view set is a single class that can handle the view, add, change,
4 | and delete operations which each require dedicated views under the UI.
5 | """
6 |
7 | from django.db.models import Count
8 | from netbox.api.viewsets import NetBoxModelViewSet
9 |
10 | from .. import filtersets, models
11 | from .serializers import (
12 | AccessListSerializer,
13 | ACLExtendedRuleSerializer,
14 | ACLInterfaceAssignmentSerializer,
15 | ACLStandardRuleSerializer,
16 | )
17 |
18 | __all__ = [
19 | "AccessListViewSet",
20 | "ACLStandardRuleViewSet",
21 | "ACLInterfaceAssignmentViewSet",
22 | "ACLExtendedRuleViewSet",
23 | ]
24 |
25 |
26 | class AccessListViewSet(NetBoxModelViewSet):
27 | """
28 | Defines the view set for the django AccessList model & associates it to a view.
29 | """
30 |
31 | queryset = (
32 | models.AccessList.objects.prefetch_related("tags")
33 | .annotate(
34 | rule_count=Count("aclextendedrules") + Count("aclstandardrules"),
35 | )
36 | .prefetch_related("tags")
37 | )
38 | serializer_class = AccessListSerializer
39 | filterset_class = filtersets.AccessListFilterSet
40 |
41 |
42 | class ACLInterfaceAssignmentViewSet(NetBoxModelViewSet):
43 | """
44 | Defines the view set for the django ACLInterfaceAssignment model & associates it to a view.
45 | """
46 |
47 | queryset = models.ACLInterfaceAssignment.objects.prefetch_related(
48 | "access_list",
49 | "tags",
50 | )
51 | serializer_class = ACLInterfaceAssignmentSerializer
52 | filterset_class = filtersets.ACLInterfaceAssignmentFilterSet
53 |
54 |
55 | class ACLStandardRuleViewSet(NetBoxModelViewSet):
56 | """
57 | Defines the view set for the django ACLStandardRule model & associates it to a view.
58 | """
59 |
60 | queryset = models.ACLStandardRule.objects.prefetch_related(
61 | "access_list",
62 | "tags",
63 | "source_prefix",
64 | )
65 | serializer_class = ACLStandardRuleSerializer
66 | filterset_class = filtersets.ACLStandardRuleFilterSet
67 |
68 |
69 | class ACLExtendedRuleViewSet(NetBoxModelViewSet):
70 | """
71 | Defines the view set for the django ACLExtendedRule model & associates it to a view.
72 | """
73 |
74 | queryset = models.ACLExtendedRule.objects.prefetch_related(
75 | "access_list",
76 | "tags",
77 | "source_prefix",
78 | "destination_prefix",
79 | )
80 | serializer_class = ACLExtendedRuleSerializer
81 | filterset_class = filtersets.ACLExtendedRuleFilterSet
82 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v5.0.0
5 | hooks:
6 | - id: check-docstring-first
7 | - id: check-merge-conflict
8 | - id: check-yaml
9 | - id: debug-statements
10 | - id: end-of-file-fixer
11 | - id: name-tests-test
12 | args:
13 | - "--django"
14 | - id: requirements-txt-fixer
15 | - id: trailing-whitespace
16 | - repo: https://github.com/PyCQA/isort
17 | rev: 6.0.1
18 | hooks:
19 | - id: isort
20 | args:
21 | - "--profile=black"
22 | exclude: ^.devcontainer/
23 | - repo: https://github.com/psf/black
24 | rev: 25.1.0
25 | hooks:
26 | - id: black
27 | language_version: python3
28 | exclude: ^.devcontainer/
29 | - repo: https://github.com/asottile/add-trailing-comma
30 | rev: v3.1.0
31 | hooks:
32 | - id: add-trailing-comma
33 | args:
34 | - "--py36-plus"
35 | - repo: https://github.com/PyCQA/flake8
36 | rev: 7.2.0
37 | hooks:
38 | - id: flake8
39 | exclude: ^.devcontainer/
40 | - repo: https://github.com/asottile/pyupgrade
41 | rev: v3.19.1
42 | hooks:
43 | - id: pyupgrade
44 | args:
45 | - "--py310-plus"
46 | - repo: https://github.com/adrienverge/yamllint
47 | rev: v1.37.1
48 | hooks:
49 | - id: yamllint
50 | - repo: https://github.com/econchick/interrogate
51 | rev: 1.7.0
52 | hooks:
53 | - id: interrogate
54 | args: [--fail-under=90, --verbose]
55 | exclude: (^.devcontainer/|^netbox_acls/migrations/)
56 | #- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
57 | # rev: v1.1.2
58 | # hooks:
59 | # - id: htmlhint
60 | # args: [--config, .htmlhintrc]
61 | - repo: https://github.com/igorshubovych/markdownlint-cli
62 | rev: v0.44.0
63 | hooks:
64 | - id: markdownlint
65 | - repo: https://github.com/astral-sh/ruff-pre-commit
66 | rev: v0.11.10
67 | hooks:
68 | # Run the linter.
69 | - id: ruff
70 | args: [--fix]
71 | # Run the formatter.
72 | - id: ruff-format
73 | #- repo: local
74 | # hooks:
75 | # - id: wily
76 | # name: wily
77 | # entry: wily diff
78 | # verbose: true
79 | # language: python
80 | # additional_dependencies: [wily]
81 | #- repo: https://github.com/sourcery-ai/sourcery
82 | # rev: v1.0.4b23
83 | # hooks:
84 | # - id: sourcery
85 | # # The best way to use Sourcery in a pre-commit hook:
86 | # # * review only changed lines:
87 | # # * omit the summary
88 | # args:
89 | # - --diff=git diff HEAD
90 | # - --no-summary
91 | ...
92 |
--------------------------------------------------------------------------------
/netbox_acls/templates/netbox_acls/accesslist.html:
--------------------------------------------------------------------------------
1 | {% extends 'generic/object.html' %}
2 | {% load render_table from django_tables2 %}
3 | {% load helpers %}
4 |
5 | {% block extra_controls %}
6 | {% if perms.netbox_acls.change_policy %}
7 | {% with viewname=object|viewname:"" %}
8 | {% if object.type == 'extended' %}
9 |
10 | {% elif object.type == 'standard' %}
11 |
12 | {% endif %}
13 | Rule
14 |
15 | {% endwith %}
16 | {% endif %}
17 | {% endblock extra_controls %}
18 |
19 | {% block content %}
20 |
21 |
22 |
23 |
24 |
25 |
26 | | Type |
27 | {% badge object.get_type_display bg_color=object.get_type_color %} |
28 |
29 |
30 | | Default Action |
31 | {% badge object.get_default_action_display bg_color=object.get_default_action_color %} |
32 |
33 |
34 | | Rules |
35 | {% if object.type == 'standard' %}
36 |
37 | {{ object.aclstandardrules.count|placeholder }}
38 | |
39 | {% elif object.type == 'extended' %}
40 |
41 | {{ object.aclextendedrules.count|placeholder }}
42 | |
43 | {% endif %}
44 |
45 |
46 | | Assigned Host |
47 | {{ object.assigned_object|linkify }} |
48 |
49 |
50 |
51 | {% include 'inc/panels/custom_fields.html' %}
52 |
53 |
54 | {% include 'inc/panels/tags.html' %}
55 | {% include 'inc/panels/comments.html' %}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {% render_table rules_table %}
64 |
65 |
66 |
67 |
68 | {% endblock content %}
69 |
--------------------------------------------------------------------------------
/netbox_acls/graphql/filters/access_list_rules.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from typing import TYPE_CHECKING, Annotated
3 |
4 | import strawberry
5 | import strawberry_django
6 | from netbox.graphql.filter_mixins import NetBoxModelFilterMixin
7 | from strawberry.scalars import ID
8 | from strawberry_django import FilterLookup
9 |
10 | from ... import models
11 |
12 | if TYPE_CHECKING:
13 | from ipam.graphql.filters import PrefixFilter
14 | from netbox.graphql.filter_lookups import IntegerArrayLookup, IntegerLookup
15 |
16 | from ..enums import (
17 | ACLProtocolEnum,
18 | ACLRuleActionEnum,
19 | )
20 | from .access_lists import AccessListFilter
21 |
22 |
23 | __all__ = (
24 | "ACLStandardRuleFilter",
25 | "ACLExtendedRuleFilter",
26 | )
27 |
28 |
29 | @dataclass
30 | class ACLRuleFilterMixin(NetBoxModelFilterMixin):
31 | """
32 | Base GraphQL filter mixin for ACL Rule models.
33 | """
34 |
35 | access_list: Annotated["AccessListFilter", strawberry.lazy("netbox_acls.graphql.filters")] | None = (
36 | strawberry_django.filter_field()
37 | )
38 | access_list_id: ID | None = strawberry_django.filter_field()
39 | index: Annotated["IntegerLookup", strawberry.lazy("netbox.graphql.filter_lookups")] | None = (
40 | strawberry_django.filter_field()
41 | )
42 | remark: FilterLookup[str] | None = strawberry_django.filter_field()
43 | description: FilterLookup[str] | None = strawberry_django.filter_field()
44 | action: Annotated["ACLRuleActionEnum", strawberry.lazy("netbox_acls.graphql.enums")] | None = (
45 | strawberry_django.filter_field()
46 | )
47 | source_prefix: Annotated["PrefixFilter", strawberry.lazy("ipam.graphql.filters")] | None = (
48 | strawberry_django.filter_field()
49 | )
50 |
51 |
52 | @strawberry_django.filter(models.ACLStandardRule, lookups=True)
53 | class ACLStandardRuleFilter(ACLRuleFilterMixin):
54 | """
55 | GraphQL filter definition for the ACLStandardRule model.
56 | """
57 |
58 | pass
59 |
60 |
61 | @strawberry_django.filter(models.ACLExtendedRule, lookups=True)
62 | class ACLExtendedRuleFilter(ACLRuleFilterMixin):
63 | """
64 | GraphQL filter definition for the ACLExtendedRule model.
65 | """
66 |
67 | source_ports: Annotated["IntegerArrayLookup", strawberry.lazy("netbox.graphql.filter_lookups")] | None = (
68 | strawberry_django.filter_field()
69 | )
70 | destination_prefix: Annotated["PrefixFilter", strawberry.lazy("ipam.graphql.filters")] | None = (
71 | strawberry_django.filter_field()
72 | )
73 | destination_ports: Annotated["IntegerArrayLookup", strawberry.lazy("netbox.graphql.filter_lookups")] | None = (
74 | strawberry_django.filter_field()
75 | )
76 | protocol: Annotated["ACLProtocolEnum", strawberry.lazy("netbox_acls.graphql.enums")] | None = (
77 | strawberry_django.filter_field()
78 | )
79 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # For most projects, this workflow file will not need changing; you simply need
3 | # to commit it to your repository.
4 | #
5 | # You may wish to alter this file to override the set of languages analyzed,
6 | # or to provide custom queries or build logic.
7 | #
8 | # ******** NOTE ********
9 | # We have attempted to detect the languages in your repository. Please check
10 | # the `language` matrix defined below to confirm you have the correct set of
11 | # supported CodeQL languages.
12 | #
13 | name: "CodeQL"
14 |
15 | on:
16 | push:
17 | branches: ["dev"]
18 | pull_request:
19 | # The branches below must be a subset of the branches above
20 | branches: ["dev"]
21 | schedule:
22 | - cron: '24 4 * * 5'
23 |
24 | jobs:
25 | analyze:
26 | name: Analyze
27 | runs-on: ubuntu-latest
28 | permissions:
29 | actions: read
30 | contents: read
31 | security-events: write
32 |
33 | strategy:
34 | fail-fast: false
35 | matrix:
36 | language: ['python']
37 | # CodeQL supports ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby']
38 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v4
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v3
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 |
53 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
54 | # queries: security-extended,security-and-quality
55 |
56 |
57 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
58 | # If this step fails, then you should remove it and run the build manually (see below)
59 | - name: Autobuild
60 | uses: github/codeql-action/autobuild@v3
61 |
62 | # ℹ️ Command-line programs to run using the OS shell.
63 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
64 |
65 | # If the Autobuild fails above, remove it and uncomment the following three lines.
66 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
67 |
68 | # - run: |
69 | # echo "Run, Build Application using script"
70 | # ./location_of_script_within_repo/buildscript.sh
71 |
72 | - name: Perform CodeQL Analysis
73 | uses: github/codeql-action/analyze@v3
74 |
--------------------------------------------------------------------------------
/netbox_acls/navigation.py:
--------------------------------------------------------------------------------
1 | """
2 | Define the plugin menu buttons and the plugin navigation bar entries.
3 | """
4 |
5 | from django.conf import settings
6 | from netbox.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
7 |
8 | plugin_settings = settings.PLUGINS_CONFIG["netbox_acls"]
9 |
10 | #
11 | # Define plugin menu buttons
12 | #
13 |
14 | # Access List
15 | accesslist_item = PluginMenuItem(
16 | link="plugins:netbox_acls:accesslist_list",
17 | link_text="Access Lists",
18 | permissions=["netbox_acls.view_accesslist"],
19 | buttons=(
20 | PluginMenuButton(
21 | link="plugins:netbox_acls:accesslist_add",
22 | title="Add",
23 | icon_class="mdi mdi-plus-thick",
24 | permissions=["netbox_acls.add_accesslist"],
25 | ),
26 | ),
27 | )
28 |
29 | # ACL Standard Rule
30 | aclstandardrule_item = PluginMenuItem(
31 | link="plugins:netbox_acls:aclstandardrule_list",
32 | link_text="Standard Rules",
33 | permissions=["netbox_acls.view_aclstandardrule"],
34 | buttons=(
35 | PluginMenuButton(
36 | link="plugins:netbox_acls:aclstandardrule_add",
37 | title="Add",
38 | icon_class="mdi mdi-plus-thick",
39 | permissions=["netbox_acls.add_aclstandardrule"],
40 | ),
41 | ),
42 | )
43 |
44 | # ACL Extended Rule
45 | aclextendedrule_item = PluginMenuItem(
46 | link="plugins:netbox_acls:aclextendedrule_list",
47 | link_text="Extended Rules",
48 | permissions=["netbox_acls.view_aclextendedrule"],
49 | buttons=(
50 | PluginMenuButton(
51 | link="plugins:netbox_acls:aclextendedrule_add",
52 | title="Add",
53 | icon_class="mdi mdi-plus-thick",
54 | permissions=["netbox_acls.add_aclextendedrule"],
55 | ),
56 | ),
57 | )
58 |
59 | # ACL Interface Assignment
60 | aclinterfaceassignment_item = PluginMenuItem(
61 | link="plugins:netbox_acls:aclinterfaceassignment_list",
62 | link_text="Interface Assignments",
63 | permissions=["netbox_acls.view_aclinterfaceassignment"],
64 | buttons=(
65 | PluginMenuButton(
66 | link="plugins:netbox_acls:aclinterfaceassignment_add",
67 | title="Add",
68 | icon_class="mdi mdi-plus-thick",
69 | permissions=["netbox_acls.add_aclinterfaceassignment"],
70 | ),
71 | ),
72 | )
73 |
74 |
75 | if plugin_settings.get("top_level_menu"):
76 | menu = PluginMenu(
77 | label="Access Lists",
78 | groups=(
79 | (
80 | "Access Lists",
81 | (accesslist_item,),
82 | ),
83 | (
84 | "Rules",
85 | (
86 | aclstandardrule_item,
87 | aclextendedrule_item,
88 | ),
89 | ),
90 | (
91 | "Assignments",
92 | (aclinterfaceassignment_item,),
93 | ),
94 | ),
95 | icon_class="mdi mdi-lock",
96 | )
97 | else:
98 | menu_items = (
99 | accesslist_item,
100 | aclstandardrule_item,
101 | aclextendedrule_item,
102 | aclinterfaceassignment_item,
103 | )
104 |
--------------------------------------------------------------------------------
/netbox_acls/graphql/types.py:
--------------------------------------------------------------------------------
1 | """
2 | Define the object types and queries available via the graphql api.
3 | """
4 |
5 | from typing import Annotated, List, Union
6 |
7 | import strawberry
8 | import strawberry_django
9 | from netbox.graphql.types import NetBoxObjectType
10 |
11 | from .. import models
12 | from . import filters
13 |
14 |
15 | @strawberry_django.type(
16 | models.AccessList,
17 | fields="__all__",
18 | exclude=["assigned_object_type", "assigned_object_id"],
19 | filters=filters.AccessListFilter,
20 | )
21 | class AccessListType(NetBoxObjectType):
22 | """
23 | Defines the object type for the django model AccessList.
24 | """
25 |
26 | # Model fields
27 | assigned_object_type: Annotated["ContentTypeType", strawberry.lazy("netbox.graphql.types")]
28 | assigned_object: Annotated[
29 | Union[
30 | Annotated["DeviceType", strawberry.lazy("dcim.graphql.types")],
31 | Annotated["VirtualMachineType", strawberry.lazy("virtualization.graphql.types")],
32 | ],
33 | strawberry.union("ACLAssignmentType"),
34 | ]
35 |
36 | # Related models
37 | aclstandardrules: List[
38 | Annotated[
39 | "ACLStandardRuleType",
40 | strawberry.lazy("netbox_acls.graphql.types"),
41 | ]
42 | ]
43 | aclextendedrules: List[
44 | Annotated[
45 | "ACLExtendedRuleType",
46 | strawberry.lazy("netbox_acls.graphql.types"),
47 | ]
48 | ]
49 |
50 |
51 | @strawberry_django.type(
52 | models.ACLInterfaceAssignment,
53 | fields="__all__",
54 | exclude=["assigned_object_type", "assigned_object_id"],
55 | filters=filters.ACLInterfaceAssignmentFilter,
56 | )
57 | class ACLInterfaceAssignmentType(NetBoxObjectType):
58 | """
59 | Defines the object type for the django model ACLInterfaceAssignment.
60 | """
61 |
62 | # Model fields
63 | access_list: Annotated["AccessListType", strawberry.lazy("netbox_acls.graphql.types")]
64 | assigned_object_type: Annotated["ContentTypeType", strawberry.lazy("netbox.graphql.types")]
65 | assigned_object: Annotated[
66 | Union[
67 | Annotated["InterfaceType", strawberry.lazy("dcim.graphql.types")],
68 | Annotated["VMInterfaceType", strawberry.lazy("virtualization.graphql.types")],
69 | ],
70 | strawberry.union("ACLInterfaceAssignedObjectType"),
71 | ]
72 |
73 |
74 | @strawberry_django.type(
75 | models.ACLStandardRule,
76 | fields="__all__",
77 | filters=filters.ACLStandardRuleFilter,
78 | )
79 | class ACLStandardRuleType(NetBoxObjectType):
80 | """
81 | Defines the object type for the django model ACLStandardRule.
82 | """
83 |
84 | # Model fields
85 | access_list: Annotated["AccessListType", strawberry.lazy("netbox_acls.graphql.types")]
86 | source_prefix: Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] | None
87 |
88 |
89 | @strawberry_django.type(
90 | models.ACLExtendedRule,
91 | fields="__all__",
92 | filters=filters.ACLExtendedRuleFilter,
93 | )
94 | class ACLExtendedRuleType(NetBoxObjectType):
95 | """
96 | Defines the object type for the django model ACLExtendedRule.
97 | """
98 |
99 | # Model fields
100 | access_list: Annotated["AccessListType", strawberry.lazy("netbox_acls.graphql.types")]
101 | source_prefix: Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] | None
102 | source_ports: List[int] | None
103 | destination_prefix: Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] | None
104 | destination_ports: List[int] | None
105 |
--------------------------------------------------------------------------------
/netbox_acls/tests/models/base.py:
--------------------------------------------------------------------------------
1 | from dcim.models import (
2 | Device,
3 | DeviceRole,
4 | DeviceType,
5 | Manufacturer,
6 | Site,
7 | VirtualChassis,
8 | )
9 | from django.test import TestCase
10 | from ipam.models import Prefix
11 | from virtualization.models import Cluster, ClusterType, VirtualMachine
12 |
13 |
14 | class BaseTestCase(TestCase):
15 | """
16 | Base test case for netbox_acls models.
17 | """
18 |
19 | @classmethod
20 | def setUpTestData(cls):
21 | """
22 | Create base data to test using including
23 | - 1 of each of the following: test site, manufacturer, device type
24 | device role, cluster type, cluster, virtual chassis, and
25 | virtual machine
26 | - 2 of each Device, prefix
27 | """
28 |
29 | # Sites
30 | site = Site.objects.create(
31 | name="Site 1",
32 | slug="site-1",
33 | )
34 |
35 | # Device Types
36 | manufacturer = Manufacturer.objects.create(
37 | name="Manufacturer 1",
38 | slug="manufacturer-1",
39 | )
40 | device_type = DeviceType.objects.create(
41 | manufacturer=manufacturer,
42 | model="Device Type 1",
43 | )
44 |
45 | # Device Roles
46 | device_role = DeviceRole.objects.create(
47 | name="Device Role 1",
48 | slug="device-role-1",
49 | )
50 |
51 | # Devices
52 | cls.device1 = Device.objects.create(
53 | name="Device 1",
54 | site=site,
55 | device_type=device_type,
56 | role=device_role,
57 | )
58 | cls.device2 = Device.objects.create(
59 | name="Device 2",
60 | site=site,
61 | device_type=device_type,
62 | role=device_role,
63 | )
64 |
65 | # Virtual Chassis
66 | cls.virtual_chassis1 = VirtualChassis.objects.create(
67 | name="Virtual Chassis 1",
68 | )
69 |
70 | # Virtual Chassis Members
71 | cls.virtual_chassis_member1 = Device.objects.create(
72 | name="VC Device",
73 | site=site,
74 | device_type=device_type,
75 | role=device_role,
76 | virtual_chassis=cls.virtual_chassis1,
77 | vc_position=1,
78 | )
79 |
80 | # Virtualization Cluster Type
81 | cluster_type = ClusterType.objects.create(
82 | name="Cluster Type 1",
83 | )
84 |
85 | # Virtualization Cluster
86 | cluster = Cluster.objects.create(
87 | name="Cluster 1",
88 | type=cluster_type,
89 | )
90 |
91 | # Virtualization Cluster Member
92 | cls.cluster_member1 = Device.objects.create(
93 | name="Cluster Device",
94 | site=site,
95 | device_type=device_type,
96 | role=device_role,
97 | )
98 |
99 | # Virtual Machine
100 | cls.virtual_machine1 = VirtualMachine.objects.create(
101 | name="VirtualMachine 1",
102 | status="active",
103 | cluster=cluster,
104 | )
105 | cls.virtual_machine2 = VirtualMachine.objects.create(
106 | name="VirtualMachine 2",
107 | status="active",
108 | cluster=cluster,
109 | )
110 |
111 | # Prefix
112 | cls.prefix1 = Prefix.objects.create(
113 | prefix="10.1.0.0/16",
114 | )
115 | cls.prefix2 = Prefix.objects.create(
116 | prefix="10.2.0.0/16",
117 | )
118 |
--------------------------------------------------------------------------------
/netbox_acls/forms/bulk_edit.py:
--------------------------------------------------------------------------------
1 | """
2 | Draft for a possible BulkEditForm, but may not be worth wile.
3 | """
4 |
5 | # from dcim.models import Device, Region, Site, SiteGroup, VirtualChassis
6 | # from django import forms
7 | # from django.core.exceptions import ValidationError
8 | # from django.utils.safestring import mark_safe
9 | # from netbox.forms import NetBoxModelBulkEditForm
10 | # from utilities.forms.utils import add_blank_choice
11 | # from utilities.forms.fields import (
12 | # ChoiceField,
13 | # DynamicModelChoiceField,
14 | # StaticSelect,
15 | # )
16 | # from virtualization.models import VirtualMachine
17 |
18 | # from ..choices import ACLActionChoices, ACLTypeChoices
19 | # from ..models import AccessList
20 |
21 |
22 | # __all__ = (
23 | # 'AccessListBulkEditForm',
24 | # )
25 |
26 |
27 | # class AccessListBulkEditForm(NetBoxModelBulkEditForm):
28 | # model = AccessList
29 | #
30 | # region = DynamicModelChoiceField(
31 | # queryset=Region.objects.all(),
32 | # required=False,
33 | # )
34 | # site_group = DynamicModelChoiceField(
35 | # queryset=SiteGroup.objects.all(),
36 | # required=False,
37 | # label='Site Group'
38 | # )
39 | # site = DynamicModelChoiceField(
40 | # queryset=Site.objects.all(),
41 | # required=False
42 | # )
43 | # device = DynamicModelChoiceField(
44 | # queryset=Device.objects.all(),
45 | # query_params={
46 | # 'region': '$region',
47 | # 'group_id': '$site_group',
48 | # 'site_id': '$site',
49 | # },
50 | # required=False,
51 | # )
52 | # type = ChoiceField(
53 | # choices=add_blank_choice(ACLTypeChoices),
54 | # required=False,
55 | # widget=StaticSelect(),
56 | # )
57 | # default_action = ChoiceField(
58 | # choices=add_blank_choice(ACLActionChoices),
59 | # required=False,
60 | # widget=StaticSelect(),
61 | # label='Default Action',
62 | # )
63 | #
64 | # fieldsets = [
65 | # ('Host Details', ('region', 'site_group', 'site', 'device')),
66 | # ('Access List Details', ('type', 'default_action', 'add_tags', 'remove_tags')),
67 | # ]
68 | #
69 | #
70 | # class Meta:
71 | # model = AccessList
72 | # fields = ('region', 'site_group', 'site', 'device', 'type', 'default_action', 'add_tags', 'remove_tags')
73 | # help_texts = {
74 | # 'default_action': 'The default behavior of the ACL.',
75 | # 'name': 'The name uniqueness per device is case insensitive.',
76 | # 'type': mark_safe('*Note: CANNOT be changed if ACL Rules are assoicated to this Access List.'),
77 | # }
78 | #
79 | # def clean(self): # Not working given you are bulkd editing multiple forms
80 | # cleaned_data = super().clean()
81 | # if self.errors.get('name'):
82 | # return cleaned_data
83 | # name = cleaned_data.get('name')
84 | # device = cleaned_data.get('device')
85 | # type = cleaned_data.get('type')
86 | # if ('name' in self.changed_data or 'device' in self.changed_data) and AccessList.objects.filter(name__iexact=name, device=device).exists():
87 | # raise forms.ValidationError('An ACL with this name (case insensitive) is already associated to this device.')
88 | # if type == 'extended' and self.cleaned_data['aclstandardrules'].exists():
89 | # raise forms.ValidationError('This ACL has Standard ACL rules already associated, CANNOT change ACL type!!')
90 | # elif type == 'standard' and self.cleaned_data['aclextendedrules'].exists():
91 | # raise forms.ValidationError('This ACL has Extended ACL rules already associated, CANNOT change ACL type!!')
92 | # return cleaned_data
93 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | #.env
124 | .venv
125 | #env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 | # VS Code
163 | .vscode/
164 | # JetBrains
165 | .idea/
166 |
167 | # Temporary files
168 | *.tmp
169 | tmp/
170 |
171 | # coverage
172 | coverage/
173 | htmlcov/
174 | .coverage
175 | .coverage.*
176 | coverage.xml
177 | *.cover
178 |
179 | # ruff
180 | .ruff_cache/
181 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME=netbox_acls
2 | REPO_PATH=/opt/netbox/netbox/netbox-acls
3 | VENV_PY_PATH=/opt/netbox/venv/bin/python3
4 | NETBOX_MANAGE_PATH=/opt/netbox/netbox
5 | NETBOX_INITIALIZER_PATH=${NETBOX_MANAGE_PATH}/netbox_initializers/
6 | VERFILE=./version.py
7 |
8 | .PHONY: help ## Display help message
9 | help:
10 | @grep -E '^[0-9a-zA-Z_-]+\.*[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
11 |
12 | ##################
13 | ## DOCKER #
14 | ##################
15 | #
16 | ## Outside of Devcontainer
17 | #
18 | #.PHONY: cleanup ## Clean associated docker resources.
19 | #cleanup:
20 | # -docker-compose -p netbox-acls_devcontainer rm -fv
21 |
22 | ##################
23 | # PLUGIN DEV #
24 | ##################
25 |
26 | # in VS Code Devcontianer
27 |
28 | .PHONY: nbshell ## Run nbshell
29 | nbshell:
30 | ${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py nbshell
31 | from netbox_acls.models import *
32 |
33 | .PHONY: setup ## Copy plugin settings. Setup NetBox plugin.
34 | setup:
35 | -${VENV_PY_PATH} -m pip install --disable-pip-version-check --no-cache-dir -e ${REPO_PATH}
36 | #-python3 setup.py develop
37 |
38 | .PHONY: example_initializers ## Run initializers
39 | example_initializers:
40 | -${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py copy_initializers_examples --path /opt/netbox/netbox/netbox-acls/.devcontainer/initializers
41 |
42 | .PHONY: load_initializers ## Run initializers
43 | load_initializers:
44 | -${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py load_initializer_data --path /opt/netbox/netbox/netbox-acls/.devcontainer/initializers
45 |
46 | .PHONY: makemigrations ## Run makemigrations
47 | makemigrations:
48 | -${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py makemigrations --name ${PLUGIN_NAME}
49 |
50 | .PHONY: migrate ## Run migrate
51 | migrate:
52 | -${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py migrate
53 |
54 | .PHONY: collectstatic
55 | collectstatic:
56 | -${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py collectstatic --no-input
57 |
58 | .PHONY: initializers
59 | initializers:
60 | -rm -rf ${NETBOX_INITIALIZER_PATH}
61 | -mkdir ${NETBOX_INITIALIZER_PATH}
62 | -${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py copy_initializers_examples --path ${NETBOX_INITIALIZER_PATH}
63 | -for file in ${NETBOX_INITIALIZER_PATH}/*.yml; do sed -i "s/^# //g" "$$file"; done
64 | -${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py load_initializer_data --path ${NETBOX_INITIALIZER_PATH}
65 |
66 | .PHONY: start ## Start NetBox
67 | start:
68 | - cd /opt/netbox/netbox/ && /opt/netbox/docker-entrypoint.sh && /opt/netbox/launch-netbox.sh
69 |
70 | .PHONY: all ## Run all PLUGIN DEV targets
71 | all: setup makemigrations migrate collectstatic initializers start
72 |
73 | .PHONY: rebuild ## Run PLUGIN DEV targets to rebuild
74 | rebuild: setup makemigrations migrate collectstatic start
75 |
76 | .PHONY: test
77 | test: setup
78 | ${NETBOX_MANAGE_PATH}/manage.py makemigrations ${PLUGIN_NAME} --check
79 | coverage run --source "netbox_acls" ${NETBOX_MANAGE_PATH}/manage.py test ${PLUGIN_NAME} -v 2
80 |
81 | .PHONY: coverage_report
82 | coverage_report:
83 | coverage report
84 |
85 | .PHONY: test_coverage
86 | test_coverage: test coverage_report
87 |
88 | #relpatch:
89 | # $(eval GSTATUS := $(shell git status --porcelain))
90 | #ifneq ($(GSTATUS),)
91 | # $(error Git status is not clean. $(GSTATUS))
92 | #endif
93 | # git checkout develop
94 | # git remote update
95 | # git pull origin develop
96 | # $(eval CURVER := $(shell cat $(VERFILE) | grep -oE '[0-9]+\.[0-9]+\.[0-9]+'))
97 | # $(eval NEWVER := $(shell pysemver bump patch $(CURVER)))
98 | # $(eval RDATE := $(shell date '+%Y-%m-%d'))
99 | # git checkout -b release-$(NEWVER) origin/develop
100 | # echo '__version__ = "$(NEWVER)"' > $(VERFILE)
101 | # git commit -am 'bump ver'
102 | # git push origin release-$(NEWVER)
103 | # git checkout develop
104 |
105 | #pbuild:
106 | # ${VENV_PY_PATH} -m pip install --upgrade build
107 | # ${VENV_PY_PATH} -m build
108 | #
109 | #pypipub:
110 | # ${VENV_PY_PATH} -m pip install --upgrade twine
111 | # ${VENV_PY_PATH} -m twine upload dist/*
112 |
--------------------------------------------------------------------------------
/.devcontainer/.bashrc:
--------------------------------------------------------------------------------
1 | # If you come from bash you might have to change your $PATH.
2 | # export PATH=$HOME/bin:/usr/local/bin:$PATH
3 |
4 | # Path to your oh-my-zsh installation.
5 | export ZSH="$HOME/.oh-my-zsh"
6 |
7 | # Set name of the theme to load --- if set to "random", it will
8 | # load a random theme each time oh-my-zsh is loaded, in which case,
9 | # to know which specific one was loaded, run: echo $RANDOM_THEME
10 | # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
11 | ZSH_THEME="robbyrussell"
12 |
13 | # Set list of themes to pick from when loading at random
14 | # Setting this variable when ZSH_THEME=random will cause zsh to load
15 | # a theme from this variable instead of looking in $ZSH/themes/
16 | # If set to an empty array, this variable will have no effect.
17 | # ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )
18 |
19 | # Uncomment the following line to use case-sensitive completion.
20 | # CASE_SENSITIVE="true"
21 |
22 | # Uncomment the following line to use hyphen-insensitive completion.
23 | # Case-sensitive completion must be off. _ and - will be interchangeable.
24 | # HYPHEN_INSENSITIVE="true"
25 |
26 | # Uncomment one of the following lines to change the auto-update behavior
27 | # zstyle ':omz:update' mode disabled # disable automatic updates
28 | # zstyle ':omz:update' mode auto # update automatically without asking
29 | # zstyle ':omz:update' mode reminder # just remind me to update when it's time
30 |
31 | # Uncomment the following line to change how often to auto-update (in days).
32 | # zstyle ':omz:update' frequency 13
33 |
34 | # Uncomment the following line if pasting URLs and other text is messed up.
35 | # DISABLE_MAGIC_FUNCTIONS="true"
36 |
37 | # Uncomment the following line to disable colors in ls.
38 | # DISABLE_LS_COLORS="true"
39 |
40 | # Uncomment the following line to disable auto-setting terminal title.
41 | # DISABLE_AUTO_TITLE="true"
42 |
43 | # Uncomment the following line to enable command auto-correction.
44 | # ENABLE_CORRECTION="true"
45 |
46 | # Uncomment the following line to display red dots whilst waiting for completion.
47 | # You can also set it to another string to have that shown instead of the default red dots.
48 | # e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f"
49 | # Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765)
50 | # COMPLETION_WAITING_DOTS="true"
51 |
52 | # Uncomment the following line if you want to disable marking untracked files
53 | # under VCS as dirty. This makes repository status check for large repositories
54 | # much, much faster.
55 | # DISABLE_UNTRACKED_FILES_DIRTY="true"
56 |
57 | # Uncomment the following line if you want to change the command execution time
58 | # stamp shown in the history command output.
59 | # You can set one of the optional three formats:
60 | # "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
61 | # or set a custom format using the strftime function format specifications,
62 | # see 'man strftime' for details.
63 | # HIST_STAMPS="mm/dd/yyyy"
64 |
65 | # Would you like to use another custom folder than $ZSH/custom?
66 | # ZSH_CUSTOM=/path/to/new-custom-folder
67 |
68 | # Which plugins would you like to load?
69 | # Standard plugins can be found in $ZSH/plugins/
70 | # Custom plugins may be added to $ZSH_CUSTOM/plugins/
71 | # Example format: plugins=(rails git textmate ruby lighthouse)
72 | # Add wisely, as too many plugins slow down shell startup.
73 | plugins=(common-aliases colored-man-pages colorize docker docker-compose emoji safe-paste git git-auto-fetch git-extras history jsontools pip)
74 |
75 | source $ZSH/oh-my-zsh.sh
76 |
77 | # User configuration
78 |
79 | # export MANPATH="/usr/local/man:$MANPATH"
80 |
81 | # You may need to manually set your language environment
82 | # export LANG=en_US.UTF-8
83 |
84 | # Preferred editor for local and remote sessions
85 | # if [[ -n $SSH_CONNECTION ]]; then
86 | # export EDITOR='vim'
87 | # else
88 | # export EDITOR='mvim'
89 | # fi
90 |
91 | # Compilation flags
92 | # export ARCHFLAGS="-arch x86_64"
93 |
94 | # Set personal aliases, overriding those provided by oh-my-zsh libs,
95 | # plugins, and themes. Aliases can be placed here, though oh-my-zsh
96 | # users are encouraged to define aliases within the ZSH_CUSTOM folder.
97 | # For a full list of active aliases, run `alias`.
98 | #
99 | # Example aliases
100 | # alias zshconfig="mate ~/.zshrc"
101 | # alias ohmyzsh="mate ~/.oh-my-zsh"
102 |
103 | # Activate Python venv in terminal
104 | source /opt/netbox/venv/bin/activate
105 |
--------------------------------------------------------------------------------
/.devcontainer/.zshrc:
--------------------------------------------------------------------------------
1 | # If you come from bash you might have to change your $PATH.
2 | # export PATH=$HOME/bin:/usr/local/bin:$PATH
3 |
4 | # Path to your oh-my-zsh installation.
5 | export ZSH="$HOME/.oh-my-zsh"
6 |
7 | # Set name of the theme to load --- if set to "random", it will
8 | # load a random theme each time oh-my-zsh is loaded, in which case,
9 | # to know which specific one was loaded, run: echo $RANDOM_THEME
10 | # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
11 | ZSH_THEME="robbyrussell"
12 |
13 | # Set list of themes to pick from when loading at random
14 | # Setting this variable when ZSH_THEME=random will cause zsh to load
15 | # a theme from this variable instead of looking in $ZSH/themes/
16 | # If set to an empty array, this variable will have no effect.
17 | # ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )
18 |
19 | # Uncomment the following line to use case-sensitive completion.
20 | # CASE_SENSITIVE="true"
21 |
22 | # Uncomment the following line to use hyphen-insensitive completion.
23 | # Case-sensitive completion must be off. _ and - will be interchangeable.
24 | # HYPHEN_INSENSITIVE="true"
25 |
26 | # Uncomment one of the following lines to change the auto-update behavior
27 | # zstyle ':omz:update' mode disabled # disable automatic updates
28 | # zstyle ':omz:update' mode auto # update automatically without asking
29 | # zstyle ':omz:update' mode reminder # just remind me to update when it's time
30 |
31 | # Uncomment the following line to change how often to auto-update (in days).
32 | # zstyle ':omz:update' frequency 13
33 |
34 | # Uncomment the following line if pasting URLs and other text is messed up.
35 | # DISABLE_MAGIC_FUNCTIONS="true"
36 |
37 | # Uncomment the following line to disable colors in ls.
38 | # DISABLE_LS_COLORS="true"
39 |
40 | # Uncomment the following line to disable auto-setting terminal title.
41 | # DISABLE_AUTO_TITLE="true"
42 |
43 | # Uncomment the following line to enable command auto-correction.
44 | # ENABLE_CORRECTION="true"
45 |
46 | # Uncomment the following line to display red dots whilst waiting for completion.
47 | # You can also set it to another string to have that shown instead of the default red dots.
48 | # e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f"
49 | # Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765)
50 | # COMPLETION_WAITING_DOTS="true"
51 |
52 | # Uncomment the following line if you want to disable marking untracked files
53 | # under VCS as dirty. This makes repository status check for large repositories
54 | # much, much faster.
55 | # DISABLE_UNTRACKED_FILES_DIRTY="true"
56 |
57 | # Uncomment the following line if you want to change the command execution time
58 | # stamp shown in the history command output.
59 | # You can set one of the optional three formats:
60 | # "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
61 | # or set a custom format using the strftime function format specifications,
62 | # see 'man strftime' for details.
63 | # HIST_STAMPS="mm/dd/yyyy"
64 |
65 | # Would you like to use another custom folder than $ZSH/custom?
66 | # ZSH_CUSTOM=/path/to/new-custom-folder
67 |
68 | # Which plugins would you like to load?
69 | # Standard plugins can be found in $ZSH/plugins/
70 | # Custom plugins may be added to $ZSH_CUSTOM/plugins/
71 | # Example format: plugins=(rails git textmate ruby lighthouse)
72 | # Add wisely, as too many plugins slow down shell startup.
73 | plugins=(common-aliases colored-man-pages colorize docker docker-compose emoji safe-paste git git-auto-fetch git-extras history jsontools pip)
74 |
75 | source $ZSH/oh-my-zsh.sh
76 |
77 | # User configuration
78 |
79 | # export MANPATH="/usr/local/man:$MANPATH"
80 |
81 | # You may need to manually set your language environment
82 | # export LANG=en_US.UTF-8
83 |
84 | # Preferred editor for local and remote sessions
85 | # if [[ -n $SSH_CONNECTION ]]; then
86 | # export EDITOR='vim'
87 | # else
88 | # export EDITOR='mvim'
89 | # fi
90 |
91 | # Compilation flags
92 | # export ARCHFLAGS="-arch x86_64"
93 |
94 | # Set personal aliases, overriding those provided by oh-my-zsh libs,
95 | # plugins, and themes. Aliases can be placed here, though oh-my-zsh
96 | # users are encouraged to define aliases within the ZSH_CUSTOM folder.
97 | # For a full list of active aliases, run `alias`.
98 | #
99 | # Example aliases
100 | # alias zshconfig="mate ~/.zshrc"
101 | # alias ohmyzsh="mate ~/.oh-my-zsh"
102 |
103 | # Activate Python venv in terminal
104 | source /opt/netbox/venv/bin/activate
105 |
--------------------------------------------------------------------------------
/configuration/configuration.py:
--------------------------------------------------------------------------------
1 | ####
2 | ## We recommend to not edit this file.
3 | ## Create separate files to overwrite the settings.
4 | ## See `extra.py` as an example.
5 | ####
6 |
7 | from os import environ
8 | from os.path import abspath, dirname
9 |
10 | # For reference see https://netbox.readthedocs.io/en/stable/configuration/
11 | # Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py
12 |
13 |
14 | # Read secret from file
15 | def _read_secret(secret_name, default=None):
16 | try:
17 | f = open(f"/run/secrets/{secret_name}", encoding="utf-8")
18 | except OSError:
19 | return default
20 | else:
21 | with f:
22 | return f.readline().strip()
23 |
24 |
25 | _BASE_DIR = dirname(dirname(abspath(__file__)))
26 |
27 | #########################
28 | # #
29 | # Required settings #
30 | # #
31 | #########################
32 |
33 | # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
34 | # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
35 | #
36 | # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
37 | ALLOWED_HOSTS = environ.get("ALLOWED_HOSTS", "*").split(" ")
38 |
39 | # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
40 | # https://docs.djangoproject.com/en/stable/ref/settings/#databases
41 | DATABASE = {
42 | "NAME": environ.get("DB_NAME", "netbox"), # Database name
43 | "USER": environ.get("DB_USER", ""), # PostgreSQL username
44 | "PASSWORD": _read_secret("db_password", environ.get("DB_PASSWORD", "")),
45 | # PostgreSQL password
46 | "HOST": environ.get("DB_HOST", "localhost"), # Database server
47 | "PORT": environ.get("DB_PORT", ""), # Database port (leave blank for default)
48 | "OPTIONS": {"sslmode": environ.get("DB_SSLMODE", "prefer")},
49 | # Database connection SSLMODE
50 | "CONN_MAX_AGE": int(environ.get("DB_CONN_MAX_AGE", "300")),
51 | # Max database connection age
52 | "DISABLE_SERVER_SIDE_CURSORS": environ.get(
53 | "DB_DISABLE_SERVER_SIDE_CURSORS",
54 | "False",
55 | ).lower()
56 | == "true",
57 | # Disable the use of server-side cursors transaction pooling
58 | }
59 |
60 | # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
61 | # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
62 | # to use two separate database IDs.
63 | REDIS = {
64 | "tasks": {
65 | "HOST": environ.get("REDIS_HOST", "localhost"),
66 | "PORT": int(environ.get("REDIS_PORT", 6379)),
67 | "PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")),
68 | "DATABASE": int(environ.get("REDIS_DATABASE", 0)),
69 | "SSL": environ.get("REDIS_SSL", "False").lower() == "true",
70 | "INSECURE_SKIP_TLS_VERIFY": environ.get(
71 | "REDIS_INSECURE_SKIP_TLS_VERIFY",
72 | "False",
73 | ).lower()
74 | == "true",
75 | },
76 | "caching": {
77 | "HOST": environ.get("REDIS_CACHE_HOST", environ.get("REDIS_HOST", "localhost")),
78 | "PORT": int(environ.get("REDIS_CACHE_PORT", environ.get("REDIS_PORT", 6379))),
79 | "PASSWORD": _read_secret(
80 | "redis_cache_password",
81 | environ.get("REDIS_CACHE_PASSWORD", environ.get("REDIS_PASSWORD", "")),
82 | ),
83 | "DATABASE": int(environ.get("REDIS_CACHE_DATABASE", 1)),
84 | "SSL": environ.get("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False")).lower() == "true",
85 | "INSECURE_SKIP_TLS_VERIFY": environ.get(
86 | "REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY",
87 | environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"),
88 | ).lower()
89 | == "true",
90 | },
91 | }
92 |
93 | # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
94 | # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
95 | # symbols. NetBox will not run without this defined. For more information, see
96 | # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
97 | SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", ""))
98 |
99 | DEVELOPER = True
100 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/python-3-postgres
3 | // Update the VARIANT arg in docker-compose.yml to pick a Python version
4 | {
5 | "name": "NetBox Plugin Development",
6 | "dockerComposeFile": ["docker-compose.yml", "docker-compose.override.yml"],
7 | "service": "netbox",
8 | //"workspaceMount": "source=${localWorkspaceFolder},target=/opt/netbox/netbox/netbox-acls,type=bind,consistency=cached",
9 | "workspaceFolder": "/opt/netbox/netbox/netbox-acls",
10 |
11 | "overrideCommand": false,
12 |
13 | // Configure tool-specific properties.
14 | "customizations": {
15 | // Configure properties specific to VS Code.
16 | "vscode": {
17 | // Set *default* container specific settings.json values on container create.
18 | "settings": {
19 | "editor.experimental.stickyScroll.enabled": true,
20 | //"[python]": {
21 | // "editor.codeActionsOnSave": {
22 | // "source.organizeImports": true
23 | // }
24 | //},
25 | "isort.args": ["--profile=black"],
26 | "isort.path": ["/opt/netbox/venv/bin/isort"],
27 | "python.analysis.typeCheckingMode": "strict",
28 | "python.analysis.extraPaths": ["/opt/netbox/netbox"],
29 | "python.autoComplete.extraPaths": ["/opt/netbox/netbox"],
30 | "python.defaultInterpreterPath": "/opt/netbox/venv/bin/python3",
31 | "python.formatting.autopep8Path": "/opt/netbox/venv/bin/autopep8",
32 | "python.formatting.blackPath": "/opt/netbox/venv/bin/black",
33 | "python.formatting.provider": "black",
34 | "python.formatting.yapfPath": "/opt/netbox/venv/bin/yapf",
35 | "python.linting.banditPath": "/opt/netbox/venv/bin/bandit",
36 | "python.linting.enabled": true,
37 | "python.linting.flake8Path": "/opt/netbox/venv/bin/flake8",
38 | "python.linting.flake8Args": ["--max-line-length=160", "--ignore=E203"],
39 | "python.linting.mypyPath": "//opt/netbox/venv/bin/mypy",
40 | "python.linting.pycodestylePath": "/opt/netbox/venv/bin/pycodestyle",
41 | "python.linting.pydocstylePath": "/opt/netbox/venv/bin/pydocstyle",
42 | "python.linting.pylintArgs": [
43 | "--load-plugins",
44 | "pylint_django",
45 | "--errors-only",
46 | "--load-plugins=pylint_django",
47 | "--django-settings-module=/opt/netbox/netbox/netbox/netbox.settings",
48 | "--enable=W0602,W0611,W0612,W0613,W0614"
49 | ],
50 | "python.linting.pylintEnabled": true,
51 | "python.linting.pylintPath": "/opt/netbox/venv/bin/pylint",
52 | "python.linting.lintOnSave": true,
53 | "python.pythonPath": "/opt/netbox/venv/bin/python3",
54 | "python.terminal.activateEnvironment": true,
55 | "python.venvPath": "/opt/netbox/",
56 | "files.exclude": {
57 | "**/node_modules": true,
58 | "build": true,
59 | "dist": true,
60 | "*egg*": true
61 | }
62 | },
63 |
64 | // Add the IDs of extensions you want installed when the container is created.
65 | "extensions": [
66 | "DavidAnson.vscode-markdownlint",
67 | "GitHub.codespaces",
68 | "GitHub.copilot-labs",
69 | "GitHub.vscode-pull-request-github",
70 | "Gruntfuggly.todo-tree",
71 | "Tyriar.sort-lines",
72 | "aaron-bond.better-comments",
73 | "batisteo.vscode-django",
74 | "charliermarsh.ruff",
75 | "codezombiech.gitignore",
76 | "esbenp.prettier-vscode",
77 | "exiasr.hadolint",
78 | "formulahendry.auto-rename-tag",
79 | "mintlify.document",
80 | "ms-python.isort",
81 | "ms-python.pylint",
82 | "ms-python.python",
83 | "ms-python.vscode-pylance",
84 | "ms-vscode.makefile-tools",
85 | "mutantdino.resourcemonitor",
86 | "oderwat.indent-rainbow",
87 | "paulomenezes.duplicated-code",
88 | "redhat.vscode-yaml",
89 | "searKing.preview-vscode",
90 | "sourcery.sourcery",
91 | "wholroyd.jinja",
92 | "yzhang.markdown-all-in-one"
93 | ]
94 | }
95 | },
96 |
97 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
98 | // This can be used to network with other containers or the host.
99 | // "forwardPorts": [5000, 5432],
100 |
101 | // Use 'postCreateCommand' to run commands after the container is created.
102 | // "postCreateCommand": "pip install --user -r requirements-dev.txt",
103 |
104 | //"postAttachCommand": "source /opt/netbox/venv/bin/activate",
105 |
106 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
107 | "remoteUser": "ubuntu"
108 | }
109 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # CONTRIBUTIING
2 |
3 | ## Reporting Bugs
4 |
5 | * First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases)
6 | of NetBox or this plugin is at [latest stable version](https://github.com/ryanmerolle/netbox-acls/releases).
7 | If you're running an older version, it's possible that the bug has already been fixed
8 | or, you are running a version of the plugin not tested with the NetBox version
9 | you are running [Compatibility Matrix](./README.md#compatibility).
10 |
11 | * Next, check the GitHub [issues list](https://github.com/ryanmerolle/netbox-acls/issues)
12 | to see if the bug you've found has already been reported. If you think you may
13 | be experiencing a reported issue that hasn't already been resolved, please
14 | click "add a reaction" in the top right corner of the issue and add a thumbs
15 | up (+1). You might also want to add a comment describing how it's affecting your
16 | installation. This will allow us to prioritize bugs based on how many users are
17 | affected.
18 |
19 | * When submitting an issue, please be as descriptive as possible. Be sure to
20 | provide all information request in the issue template, including:
21 |
22 | * The environment in which NetBox is running
23 | * The exact steps that can be taken to reproduce the issue
24 | * Expected and observed behavior
25 | * Any error messages generated
26 | * Screenshots (if applicable)
27 |
28 | ## Feature Requests
29 |
30 | * First, check the GitHub [issues list](https://github.com/ryanmerolle/netbox-acls/issues)
31 | to see if the feature you're requesting is already listed. (Be sure to search
32 | closed issues as well, since some feature requests have been rejected.) If the
33 | feature you'd like to see has already been requested and is open, click "add a
34 | reaction" in the top right corner of the issue and add a thumbs up (+1). This
35 | ensures that the issue has a better chance of receiving attention. Also feel
36 | free to add a comment with any additional justification for the feature.
37 | (However, note that comments with no substance other than a "+1" will be
38 | deleted. Please use GitHub's reactions feature to indicate your support.)
39 |
40 | * Good feature requests are very narrowly defined. Be sure to thoroughly
41 | describe the functionality and data model(s) being proposed. The more effort
42 | you put into writing a feature request, the better its chance is of being
43 | implemented. Overly broad feature requests will be closed.
44 |
45 | * When submitting a feature request on GitHub, be sure to include all
46 | information requested by the issue template, including:
47 |
48 | * A detailed description of the proposed functionality
49 | * A use case for the feature; who would use it and what value it would add
50 | to NetBox
51 | * A rough description of changes necessary to the database schema (if
52 | applicable)
53 | * Any third-party libraries or other resources which would be involved
54 |
55 | ## Submitting Pull Requests
56 |
57 | * Be sure to open an issue **before** starting work on a pull request, and
58 | discuss your idea with the NetBox maintainers before beginning work. This will
59 | help prevent wasting time on something that might we might not be able to
60 | implement. When suggesting a new feature, also make sure it won't conflict with
61 | any work that's already in progress.
62 |
63 | * Once you've opened or identified an issue you'd like to work on, ask that it
64 | be assigned to you so that others are aware it's being worked on. A maintainer
65 | will then mark the issue as "accepted."
66 |
67 | * Any pull request which does _not_ relate to an **accepted** issue will be closed.
68 |
69 | * All new functionality must include relevant tests where applicable.
70 |
71 | * When submitting a pull request, please be sure to work off of the `develop`
72 | branch, rather than `main`. The `dev` branch is used for ongoing
73 | development, while `main` is used for tagging stable releases.
74 |
75 | * In most cases, it is not necessary to add a changelog entry: A maintainer will
76 | take care of this when the PR is merged. (This helps avoid merge conflicts
77 | resulting from multiple PRs being submitted simultaneously.)
78 |
79 | * All code submissions should meet the following criteria (CI will enforce
80 | these checks):
81 |
82 | * TBD
83 |
84 | ## Commenting
85 |
86 | Only comment on an issue if you are sharing a relevant idea or constructive
87 | feedback. **Do not** comment on an issue just to show your support (give the
88 | top post a :+1: instead) or ask for an ETA. These comments will be deleted to
89 | reduce noise in the discussion.
90 |
91 | ## Development Workflow / Tooling
92 |
93 | Development with this plugin leverges:
94 |
95 | * VS Code
96 | * VS Code devcontainer
97 | * NetBox-Docker
98 | * Docker-Compose
99 | * Makefile for spin up of testing NetBox setup
100 | * Dependabot for dependency version management
101 |
102 | ### Cutting Releases
103 |
104 | 1. Merge PR (squash) into `dev` branch
105 | 2. Merge `dev` into `release` branch
106 | 3. Create a release (pypi auto publishes)
107 |
108 | More Documentation to come.
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NetBox Access Lists Plugin
2 |
3 | A [Netbox](https://github.com/netbox-community/netbox) plugin for Access List management.
4 |
5 | ## Features
6 |
7 | This plugin provides the following models:
8 |
9 | - Access Lists
10 | - Access List to Interface Assignment
11 | - Access List Rules (abstract model basis for other rules)
12 | - Access List Standard Rules
13 | - Access List Extended Rules
14 |
15 | ## Origin
16 |
17 | Based on the NetBox plugin tutorial by [jeremystretch](https://github.com/jeremystretch):
18 |
19 | - [demo repository](https://github.com/netbox-community/netbox-plugin-demo)
20 | - [tutorial](https://github.com/netbox-community/netbox-plugin-tutorial)
21 |
22 | All credit should go to Jeremy. Thanks, Jeremy!
23 |
24 | This project just looks to build on top of this framework and model presented.
25 |
26 | ## Contributing
27 |
28 | This project is currently maintained by the [netbox-community](https://github.com/netbox-community).
29 |
30 | See the [CONTRIBUTING](CONTRIBUTING.md) for more information.
31 |
32 | ## Compatibility
33 |
34 | Each Plugin Version listed below has been tested with its corresponding NetBox Version.
35 |
36 | | NetBox Version | Plugin Version |
37 | |:-------------------:|:--------------:|
38 | | 4.4.x | 1.9.1 |
39 | | 4.3.x | 1.9.1 |
40 | | 4.2.x | 1.8.1 |
41 | | 4.1.x | 1.7.0 |
42 | | >= 4.0.2 < 4.1.0 | 1.6.1 |
43 | | 3.7.x | 1.5.0 |
44 | | 3.6.x | 1.4.0 |
45 | | 3.5.x | 1.3.0 |
46 | | 3.4.x | 1.2.2 |
47 | | 3.3.x | 1.1.0 |
48 | | 3.2.x | 1.0.1 |
49 |
50 | ## Installing
51 |
52 | For adding to a NetBox Docker setup see
53 | [the general instructions for using netbox-docker with plugins](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins).
54 |
55 | You can install with pip:
56 |
57 | ```bash
58 | pip install netbox-acls
59 | ```
60 |
61 | or by adding to your `local_requirements.txt` or `plugin_requirements.txt` (netbox-docker):
62 |
63 | ```bash
64 | netbox-acls
65 | ```
66 |
67 | ## Configuration
68 |
69 | Enable the plugin in `/opt/netbox/netbox/netbox/configuration.py`,
70 | or if you use netbox-docker, your `/configuration/plugins.py` file :
71 |
72 | ```python
73 | PLUGINS = [
74 | "netbox_acls"
75 | ]
76 |
77 | PLUGINS_CONFIG = {
78 | "netbox_acls": {
79 | "top_level_menu": True # If set to True the plugin will add a top level menu item for the plugin. If set to False the plugin will add a menu item under the Plugins menu item. Default is set to True.
80 | },
81 | }
82 | ```
83 |
84 | To add the required `netbox-acls` tables to your NetBox database, run the `migrate` manager subcommand in the NetBox virtual environment:
85 | ```
86 | cd /opt/netbox
87 | sudo ./venv/bin/python3 netbox/manage.py migrate
88 | ```
89 |
90 | ## Developing
91 |
92 | ### VSCode + Docker + Dev Containers
93 |
94 | 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.
95 |
96 | 1. In the WSL terminal, enter `code` to run Visual studio code.
97 | 2. Install the devcontainer extension "ms-vscode-remote.remote-containers"
98 | 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
99 | 4. If you'd like the netbox instance to be prepopulated with example data from [netbox-initializers](https://github.com/tobiasge/netbox-initializers) run `make initializers`
100 | 5. Start the netbox instance using `make all`
101 |
102 | Your netbox instance will be served under 0.0.0.0:8000, so it should now be available under localhost:8000.
103 |
104 | ## Screenshots
105 |
106 | Access List - List View
107 | 
108 |
109 | Access List (Type Extended) - Individual View
110 | 
111 |
112 | Access List (Type Standard) - Individual View
113 | 
114 |
115 | Extended Access List Rules - List View
116 | 
117 |
118 | Standard Access List Rules - List View
119 | 
120 |
121 | Access List Interface Assignments- List View
122 | 
123 |
124 | Host (device, virtual_chassis, virtual_machine) Access Lists - New Card
125 | 
126 |
127 | Host Interface (vminterface interface) Access Lists - New Card
128 | 
129 |
--------------------------------------------------------------------------------
/netbox_acls/tables.py:
--------------------------------------------------------------------------------
1 | """
2 | Define the object lists / table view for each of the plugin models.
3 | """
4 |
5 | import django_tables2 as tables
6 | from django.utils.translation import gettext_lazy as _
7 | from netbox.tables import ChoiceFieldColumn, NetBoxTable, columns
8 |
9 | from .models import AccessList, ACLExtendedRule, ACLInterfaceAssignment, ACLStandardRule
10 |
11 | __all__ = (
12 | "AccessListTable",
13 | "ACLInterfaceAssignmentTable",
14 | "ACLStandardRuleTable",
15 | "ACLExtendedRuleTable",
16 | )
17 |
18 |
19 | COL_HOST_ASSIGNMENT = """
20 | {% if record.assigned_object.device %}
21 | {{ record.assigned_object.device|placeholder }}
22 | {% else %}
23 | {{ record.assigned_object.virtual_machine|placeholder }}
24 | {% endif %}
25 | """
26 |
27 |
28 | class AccessListTable(NetBoxTable):
29 | """
30 | Defines the table view for the AccessList model.
31 | """
32 |
33 | pk = columns.ToggleColumn()
34 | id = tables.Column(
35 | linkify=True,
36 | )
37 | assigned_object = tables.Column(
38 | verbose_name=_("Assigned Host"),
39 | orderable=False,
40 | linkify=True,
41 | )
42 | name = tables.Column(
43 | linkify=True,
44 | )
45 | device = tables.Column(
46 | linkify=True,
47 | )
48 | type = ChoiceFieldColumn()
49 | default_action = ChoiceFieldColumn()
50 | rule_count = tables.Column(
51 | verbose_name=_("Rule Count"),
52 | )
53 | tags = columns.TagColumn(
54 | url_name="plugins:netbox_acls:accesslist_list",
55 | )
56 |
57 | class Meta(NetBoxTable.Meta):
58 | model = AccessList
59 | fields = (
60 | "pk",
61 | "id",
62 | "name",
63 | "assigned_object",
64 | "type",
65 | "rule_count",
66 | "default_action",
67 | "comments",
68 | "action",
69 | "tags",
70 | )
71 | default_columns = (
72 | "name",
73 | "assigned_object",
74 | "type",
75 | "rule_count",
76 | "default_action",
77 | "tags",
78 | )
79 |
80 |
81 | class ACLInterfaceAssignmentTable(NetBoxTable):
82 | """
83 | Defines the table view for the AccessList model.
84 | """
85 |
86 | pk = columns.ToggleColumn()
87 | id = tables.Column(
88 | linkify=True,
89 | )
90 | access_list = tables.Column(
91 | linkify=True,
92 | )
93 | direction = ChoiceFieldColumn()
94 | host = tables.TemplateColumn(
95 | template_code=COL_HOST_ASSIGNMENT,
96 | orderable=False,
97 | )
98 | assigned_object = tables.Column(
99 | verbose_name=_("Assigned Interface"),
100 | orderable=False,
101 | linkify=True,
102 | )
103 | tags = columns.TagColumn(
104 | url_name="plugins:netbox_acls:aclinterfaceassignment_list",
105 | )
106 |
107 | class Meta(NetBoxTable.Meta):
108 | model = ACLInterfaceAssignment
109 | fields = (
110 | "pk",
111 | "id",
112 | "access_list",
113 | "direction",
114 | "host",
115 | "assigned_object",
116 | "tags",
117 | )
118 | default_columns = (
119 | "id",
120 | "access_list",
121 | "direction",
122 | "host",
123 | "assigned_object",
124 | "tags",
125 | )
126 |
127 |
128 | class ACLStandardRuleTable(NetBoxTable):
129 | """
130 | Defines the table view for the ACLStandardRule model.
131 | """
132 |
133 | access_list = tables.Column(
134 | linkify=True,
135 | )
136 | index = tables.Column(
137 | linkify=True,
138 | )
139 | action = ChoiceFieldColumn()
140 | tags = columns.TagColumn(
141 | url_name="plugins:netbox_acls:aclstandardrule_list",
142 | )
143 |
144 | class Meta(NetBoxTable.Meta):
145 | model = ACLStandardRule
146 | fields = (
147 | "pk",
148 | "id",
149 | "access_list",
150 | "index",
151 | "action",
152 | "remark",
153 | "tags",
154 | "description",
155 | "source_prefix",
156 | )
157 | default_columns = (
158 | "access_list",
159 | "index",
160 | "action",
161 | "remark",
162 | "source_prefix",
163 | "tags",
164 | )
165 |
166 |
167 | class ACLExtendedRuleTable(NetBoxTable):
168 | """
169 | Defines the table view for the ACLExtendedRule model.
170 | """
171 |
172 | access_list = tables.Column(
173 | linkify=True,
174 | )
175 | index = tables.Column(
176 | linkify=True,
177 | )
178 | action = ChoiceFieldColumn()
179 | tags = columns.TagColumn(
180 | url_name="plugins:netbox_acls:aclextendedrule_list",
181 | )
182 | protocol = ChoiceFieldColumn()
183 |
184 | class Meta(NetBoxTable.Meta):
185 | model = ACLExtendedRule
186 | fields = (
187 | "pk",
188 | "id",
189 | "access_list",
190 | "index",
191 | "action",
192 | "remark",
193 | "tags",
194 | "description",
195 | "source_prefix",
196 | "source_ports",
197 | "destination_prefix",
198 | "destination_ports",
199 | "protocol",
200 | )
201 | default_columns = (
202 | "access_list",
203 | "index",
204 | "action",
205 | "remark",
206 | "tags",
207 | "source_prefix",
208 | "source_ports",
209 | "destination_prefix",
210 | "destination_ports",
211 | "protocol",
212 | )
213 |
--------------------------------------------------------------------------------
/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0.8 on 2023-01-21 09:00
2 |
3 | import django.core.validators
4 | import django.db.models.deletion
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("contenttypes", "0002_remove_content_type_name"),
11 | ("netbox_acls", "0001_initial"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterModelOptions(
16 | name="accesslist",
17 | options={
18 | "ordering": ["assigned_object_type", "assigned_object_id", "name"],
19 | "verbose_name": "Access List",
20 | "verbose_name_plural": "Access Lists",
21 | },
22 | ),
23 | migrations.AlterModelOptions(
24 | name="aclextendedrule",
25 | options={
26 | "ordering": ["access_list", "index"],
27 | "verbose_name": "ACL Extended Rule",
28 | "verbose_name_plural": "ACL Extended Rules",
29 | },
30 | ),
31 | migrations.AlterModelOptions(
32 | name="aclinterfaceassignment",
33 | options={
34 | "ordering": [
35 | "assigned_object_type",
36 | "assigned_object_id",
37 | "access_list",
38 | "direction",
39 | ],
40 | "verbose_name": "ACL Interface Assignment",
41 | "verbose_name_plural": "ACL Interface Assignments",
42 | },
43 | ),
44 | migrations.AlterModelOptions(
45 | name="aclstandardrule",
46 | options={
47 | "ordering": ["access_list", "index"],
48 | "verbose_name": "ACL Standard Rule",
49 | "verbose_name_plural": "ACL Standard Rules",
50 | },
51 | ),
52 | migrations.AlterField(
53 | model_name="accesslist",
54 | name="assigned_object_id",
55 | field=models.PositiveBigIntegerField(),
56 | ),
57 | migrations.AlterField(
58 | model_name="accesslist",
59 | name="assigned_object_type",
60 | field=models.ForeignKey(
61 | limit_choices_to=models.Q(
62 | models.Q(
63 | models.Q(("app_label", "dcim"), ("model", "device")),
64 | models.Q(("app_label", "dcim"), ("model", "virtualchassis")),
65 | models.Q(
66 | ("app_label", "virtualization"),
67 | ("model", "virtualmachine"),
68 | ),
69 | _connector="OR",
70 | ),
71 | ),
72 | on_delete=django.db.models.deletion.PROTECT,
73 | to="contenttypes.contenttype",
74 | ),
75 | ),
76 | migrations.AlterField(
77 | model_name="accesslist",
78 | name="name",
79 | field=models.CharField(
80 | max_length=500,
81 | validators=[
82 | django.core.validators.RegexValidator(
83 | "^[0-9a-zA-Z,-,_]*$",
84 | "Only alphanumeric, hyphens, and underscores characters are allowed.",
85 | ),
86 | ],
87 | ),
88 | ),
89 | migrations.AlterField(
90 | model_name="accesslist",
91 | name="type",
92 | field=models.CharField(max_length=30),
93 | ),
94 | migrations.AlterField(
95 | model_name="aclextendedrule",
96 | name="access_list",
97 | field=models.ForeignKey(
98 | limit_choices_to={"type": "extended"},
99 | on_delete=django.db.models.deletion.CASCADE,
100 | related_name="aclextendedrules",
101 | to="netbox_acls.accesslist",
102 | ),
103 | ),
104 | migrations.AlterField(
105 | model_name="aclextendedrule",
106 | name="remark",
107 | field=models.CharField(blank=True, default="", max_length=500),
108 | preserve_default=False,
109 | ),
110 | migrations.AlterField(
111 | model_name="aclinterfaceassignment",
112 | name="access_list",
113 | field=models.ForeignKey(
114 | on_delete=django.db.models.deletion.CASCADE,
115 | to="netbox_acls.accesslist",
116 | ),
117 | ),
118 | migrations.AlterField(
119 | model_name="aclinterfaceassignment",
120 | name="assigned_object_id",
121 | field=models.PositiveBigIntegerField(),
122 | ),
123 | migrations.AlterField(
124 | model_name="aclinterfaceassignment",
125 | name="assigned_object_type",
126 | field=models.ForeignKey(
127 | limit_choices_to=models.Q(
128 | models.Q(
129 | models.Q(("app_label", "dcim"), ("model", "interface")),
130 | models.Q(
131 | ("app_label", "virtualization"),
132 | ("model", "vminterface"),
133 | ),
134 | _connector="OR",
135 | ),
136 | ),
137 | on_delete=django.db.models.deletion.PROTECT,
138 | to="contenttypes.contenttype",
139 | ),
140 | ),
141 | migrations.AlterField(
142 | model_name="aclinterfaceassignment",
143 | name="direction",
144 | field=models.CharField(max_length=30),
145 | ),
146 | migrations.AlterField(
147 | model_name="aclstandardrule",
148 | name="access_list",
149 | field=models.ForeignKey(
150 | limit_choices_to={"type": "standard"},
151 | on_delete=django.db.models.deletion.CASCADE,
152 | related_name="aclstandardrules",
153 | to="netbox_acls.accesslist",
154 | ),
155 | ),
156 | migrations.AlterField(
157 | model_name="aclstandardrule",
158 | name="remark",
159 | field=models.CharField(blank=True, default="", max_length=500),
160 | preserve_default=False,
161 | ),
162 | ]
163 |
--------------------------------------------------------------------------------
/netbox_acls/tests/models/test_aclinterfaceassignments.py:
--------------------------------------------------------------------------------
1 | from dcim.models import Interface
2 | from django.core.exceptions import ValidationError
3 | from virtualization.models import VMInterface
4 |
5 | from netbox_acls.models import AccessList, ACLInterfaceAssignment
6 |
7 | from .base import BaseTestCase
8 |
9 |
10 | class TestACLInterfaceAssignment(BaseTestCase):
11 | """
12 | Test ACLInterfaceAssignment model.
13 | """
14 |
15 | @classmethod
16 | def setUpTestData(cls):
17 | """
18 | Extend BaseTestCase's setUpTestData() to create additional data for testing.
19 | """
20 | super().setUpTestData()
21 |
22 | interface_type = "1000baset"
23 |
24 | # Device Interfaces
25 | cls.device_interface1 = Interface.objects.create(
26 | name="Interface 1",
27 | device=cls.device1,
28 | type=interface_type,
29 | )
30 | cls.device_interface2 = Interface.objects.create(
31 | name="Interface 2",
32 | device=cls.device1,
33 | type=interface_type,
34 | )
35 |
36 | # Virtual Machine Interfaces
37 | cls.vm_interface1 = VMInterface.objects.create(
38 | name="Interface 1",
39 | virtual_machine=cls.virtual_machine1,
40 | )
41 | cls.vm_interface2 = VMInterface.objects.create(
42 | name="Interface 2",
43 | virtual_machine=cls.virtual_machine1,
44 | )
45 |
46 | def test_acl_interface_assignment_success(self):
47 | """
48 | Test that ACLInterfaceAssignment passes validation if the ACL is assigned to the host
49 | and not already assigned to the interface and direction.
50 | """
51 | device_acl = AccessList(
52 | name="STANDARD_ACL",
53 | assigned_object=self.device1,
54 | type="standard",
55 | default_action="permit",
56 | comments="STANDARD_ACL",
57 | )
58 | device_acl.save()
59 | acl_device_interface = ACLInterfaceAssignment(
60 | access_list=device_acl,
61 | direction="ingress",
62 | assigned_object=self.device_interface1,
63 | )
64 | acl_device_interface.full_clean()
65 |
66 | def test_acl_interface_assignment_fail(self):
67 | """
68 | Test that ACLInterfaceAssignment fails validation if the ACL is not
69 | assigned to the parent host.
70 | """
71 | device_acl = AccessList(
72 | name="STANDARD_ACL",
73 | assigned_object=self.device1,
74 | type="standard",
75 | default_action="permit",
76 | comments="STANDARD_ACL",
77 | )
78 | device_acl.save()
79 | acl_vm_interface = ACLInterfaceAssignment(
80 | access_list=device_acl,
81 | direction="ingress",
82 | assigned_object=self.vm_interface1,
83 | )
84 | with self.assertRaises(ValidationError):
85 | acl_vm_interface.full_clean()
86 | acl_vm_interface.save()
87 |
88 | def test_acl_vminterface_assignment_success(self):
89 | """
90 | Test that ACLInterfaceAssignment passes validation if the ACL is assigned to the host
91 | and not already assigned to the vminterface and direction.
92 | """
93 | vm_acl = AccessList(
94 | name="STANDARD_ACL",
95 | assigned_object=self.virtual_machine1,
96 | type="standard",
97 | default_action="permit",
98 | comments="STANDARD_ACL",
99 | )
100 | vm_acl.save()
101 | acl_vm_interface = ACLInterfaceAssignment(
102 | access_list=vm_acl,
103 | direction="ingress",
104 | assigned_object=self.vm_interface1,
105 | )
106 | acl_vm_interface.full_clean()
107 |
108 | def test_duplicate_assignment_fail(self):
109 | """
110 | Test that ACLInterfaceAssignment fails validation
111 | if the ACL already is assigned to the same interface and direction.
112 | """
113 | device_acl = AccessList(
114 | name="STANDARD_ACL",
115 | assigned_object=self.device1,
116 | type="standard",
117 | default_action="permit",
118 | comments="STANDARD_ACL",
119 | )
120 | device_acl.save()
121 | acl_device_interface1 = ACLInterfaceAssignment(
122 | access_list=device_acl,
123 | direction="ingress",
124 | assigned_object=self.device_interface1,
125 | )
126 | acl_device_interface1.full_clean()
127 | acl_device_interface1.save()
128 | acl_device_interface2 = ACLInterfaceAssignment(
129 | access_list=device_acl,
130 | direction="ingress",
131 | assigned_object=self.device_interface1,
132 | )
133 | with self.assertRaises(ValidationError):
134 | acl_device_interface2.full_clean()
135 |
136 | def test_acl_already_assigned_fail(self):
137 | """
138 | Test that ACLInterfaceAssignment fails validation
139 | if the interface already has an ACL assigned in the same direction.
140 | """
141 | pass
142 | # TODO: test_acl_already_assigned_fail - VM & Device
143 |
144 | def test_valid_acl_interface_assignment_choices(self):
145 | """
146 | Test that ACLInterfaceAssignment action choices using VALID choices.
147 | """
148 | valid_acl_assignment_direction_choices = ["ingress", "egress"]
149 |
150 | test_acl = AccessList(
151 | name="STANDARD_ACL",
152 | assigned_object=self.device1,
153 | type="standard",
154 | default_action="permit",
155 | comments="STANDARD_ACL",
156 | )
157 | test_acl.save()
158 |
159 | for direction_choice in valid_acl_assignment_direction_choices:
160 | valid_acl_assignment = ACLInterfaceAssignment(
161 | access_list=test_acl,
162 | direction=direction_choice,
163 | assigned_object=self.device_interface1,
164 | comments=f"VALID ACL ASSIGNMENT CHOICES USED: direction={direction_choice}",
165 | )
166 | valid_acl_assignment.full_clean()
167 |
168 | def test_invalid_acl_choices(self):
169 | """
170 | Test that ACLInterfaceAssignment action choices using INVALID choices.
171 | """
172 | invalid_acl_assignment_direction_choice = "both"
173 |
174 | test_acl = AccessList(
175 | name="STANDARD_ACL",
176 | assigned_object=self.device1,
177 | type="standard",
178 | default_action="permit",
179 | comments="STANDARD_ACL",
180 | )
181 | test_acl.save()
182 |
183 | invalid_acl_assignment_direction = ACLInterfaceAssignment(
184 | access_list=test_acl,
185 | direction=invalid_acl_assignment_direction_choice,
186 | assigned_object=self.device_interface1,
187 | comments=f"INVALID ACL DEFAULT CHOICE USED: default_action='{invalid_acl_assignment_direction_choice}'",
188 | )
189 | with self.assertRaises(ValidationError):
190 | invalid_acl_assignment_direction.full_clean()
191 |
--------------------------------------------------------------------------------
/netbox_acls/models/access_lists.py:
--------------------------------------------------------------------------------
1 | """
2 | Define the django models for this plugin.
3 | """
4 |
5 | from dcim.models import Device, Interface, VirtualChassis
6 | from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
7 | from django.contrib.contenttypes.models import ContentType
8 | from django.core.exceptions import ValidationError
9 | from django.core.validators import RegexValidator
10 | from django.db import models
11 | from django.urls import reverse
12 | from django.utils.translation import gettext_lazy as _
13 | from netbox.models import NetBoxModel
14 | from virtualization.models import VirtualMachine, VMInterface
15 |
16 | from ..choices import ACLActionChoices, ACLAssignmentDirectionChoices, ACLTypeChoices
17 | from ..constants import ACL_HOST_ASSIGNMENT_MODELS, ACL_INTERFACE_ASSIGNMENT_MODELS
18 |
19 | __all__ = (
20 | "AccessList",
21 | "ACLInterfaceAssignment",
22 | )
23 |
24 |
25 | alphanumeric_plus = RegexValidator(
26 | r"^[a-zA-Z0-9-_]+$",
27 | _("Only alphanumeric, hyphens, and underscores characters are allowed."),
28 | )
29 |
30 |
31 | class AccessList(NetBoxModel):
32 | """
33 | Model definition for Access Lists.
34 | """
35 |
36 | name = models.CharField(
37 | verbose_name=_("Name"),
38 | max_length=500,
39 | validators=[alphanumeric_plus],
40 | )
41 | assigned_object_type = models.ForeignKey(
42 | to=ContentType,
43 | on_delete=models.PROTECT,
44 | limit_choices_to=ACL_HOST_ASSIGNMENT_MODELS,
45 | verbose_name=_("Assigned Object Type"),
46 | )
47 | assigned_object_id = models.PositiveBigIntegerField()
48 | assigned_object = GenericForeignKey(
49 | ct_field="assigned_object_type",
50 | fk_field="assigned_object_id",
51 | )
52 | type = models.CharField(
53 | verbose_name=_("Type"),
54 | max_length=30,
55 | choices=ACLTypeChoices,
56 | )
57 | default_action = models.CharField(
58 | verbose_name=_("Default Action"),
59 | max_length=30,
60 | default=ACLActionChoices.ACTION_DENY,
61 | choices=ACLActionChoices,
62 | )
63 | comments = models.TextField(
64 | blank=True,
65 | )
66 |
67 | clone_fields = (
68 | "default_action",
69 | "type",
70 | )
71 |
72 | class Meta:
73 | unique_together = ["assigned_object_type", "assigned_object_id", "name"]
74 | ordering = ["assigned_object_type", "assigned_object_id", "name"]
75 | verbose_name = _("Access List")
76 | verbose_name_plural = _("Access Lists")
77 |
78 | def __str__(self):
79 | return self.name
80 |
81 | def get_absolute_url(self):
82 | """
83 | The method is a Django convention; although not strictly required,
84 | it conveniently returns the absolute URL for any particular object.
85 | """
86 | return reverse("plugins:netbox_acls:accesslist", args=[self.pk])
87 |
88 | def get_default_action_color(self):
89 | return ACLActionChoices.colors.get(self.default_action)
90 |
91 | def get_type_color(self):
92 | return ACLTypeChoices.colors.get(self.type)
93 |
94 |
95 | class ACLInterfaceAssignment(NetBoxModel):
96 | """
97 | Model definition for Access Lists associations with other Host interfaces:
98 | - VM interfaces
99 | - device interface
100 | """
101 |
102 | access_list = models.ForeignKey(
103 | to=AccessList,
104 | on_delete=models.CASCADE,
105 | verbose_name=_("Access List"),
106 | )
107 | direction = models.CharField(
108 | verbose_name=_("Direction"),
109 | max_length=30,
110 | choices=ACLAssignmentDirectionChoices,
111 | )
112 | assigned_object_type = models.ForeignKey(
113 | to=ContentType,
114 | on_delete=models.PROTECT,
115 | limit_choices_to=ACL_INTERFACE_ASSIGNMENT_MODELS,
116 | verbose_name=_("Assigned Object Type"),
117 | )
118 | assigned_object_id = models.PositiveBigIntegerField()
119 | assigned_object = GenericForeignKey(
120 | ct_field="assigned_object_type",
121 | fk_field="assigned_object_id",
122 | )
123 | comments = models.TextField(
124 | blank=True,
125 | )
126 |
127 | clone_fields = ("access_list", "direction")
128 | prerequisite_models = ("netbox_acls.AccessList",)
129 |
130 | class Meta:
131 | unique_together = [
132 | "assigned_object_type",
133 | "assigned_object_id",
134 | "access_list",
135 | "direction",
136 | ]
137 | ordering = [
138 | "assigned_object_type",
139 | "assigned_object_id",
140 | "access_list",
141 | "direction",
142 | ]
143 | verbose_name = _("ACL Interface Assignment")
144 | verbose_name_plural = _("ACL Interface Assignments")
145 |
146 | def __str__(self):
147 | return f"{self.access_list}: Interface {self.assigned_object}"
148 |
149 | def get_absolute_url(self):
150 | """
151 | The method is a Django convention; although not strictly required,
152 | it conveniently returns the absolute URL for any particular object.
153 | """
154 | return reverse(
155 | "plugins:netbox_acls:aclinterfaceassignment",
156 | args=[self.pk],
157 | )
158 |
159 | def save(self, *args, **kwargs):
160 | """Saves the current instance to the database."""
161 | # Ensure the assigned interface's host matches the host assigned to the access list.
162 | if self.assigned_object.parent_object != self.access_list.assigned_object:
163 | raise ValidationError(
164 | {
165 | "assigned_object": "The assigned object must be the same as the device assigned to it."
166 | }
167 | )
168 |
169 | super().save(*args, **kwargs)
170 |
171 | def get_direction_color(self):
172 | return ACLAssignmentDirectionChoices.colors.get(self.direction)
173 |
174 |
175 | GenericRelation(
176 | to=ACLInterfaceAssignment,
177 | content_type_field="assigned_object_type",
178 | object_id_field="assigned_object_id",
179 | related_query_name="interface",
180 | ).contribute_to_class(Interface, "accesslistassignments")
181 |
182 | GenericRelation(
183 | to=ACLInterfaceAssignment,
184 | content_type_field="assigned_object_type",
185 | object_id_field="assigned_object_id",
186 | related_query_name="vminterface",
187 | ).contribute_to_class(VMInterface, "accesslistassignments")
188 |
189 | GenericRelation(
190 | to=AccessList,
191 | content_type_field="assigned_object_type",
192 | object_id_field="assigned_object_id",
193 | related_query_name="device",
194 | ).contribute_to_class(Device, "accesslists")
195 |
196 | GenericRelation(
197 | to=AccessList,
198 | content_type_field="assigned_object_type",
199 | object_id_field="assigned_object_id",
200 | related_query_name="virtual_chassis",
201 | ).contribute_to_class(VirtualChassis, "accesslists")
202 |
203 | GenericRelation(
204 | to=AccessList,
205 | content_type_field="assigned_object_type",
206 | object_id_field="assigned_object_id",
207 | related_query_name="virtual_machine",
208 | ).contribute_to_class(VirtualMachine, "accesslists")
209 |
--------------------------------------------------------------------------------
/netbox_acls/tests/models/test_standardrules.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ValidationError
2 |
3 | from netbox_acls.choices import ACLTypeChoices
4 | from netbox_acls.models import AccessList, ACLStandardRule
5 |
6 | from .base import BaseTestCase
7 |
8 |
9 | class TestACLStandardRule(BaseTestCase):
10 | """
11 | Test ACLStandardRule model.
12 | """
13 |
14 | @classmethod
15 | def setUpTestData(cls):
16 | """
17 | Extend BaseTestCase's setUpTestData() to create additional data for testing.
18 | """
19 | super().setUpTestData()
20 |
21 | cls.acl_type = ACLTypeChoices.TYPE_STANDARD
22 | cls.default_action = "deny"
23 |
24 | # AccessLists
25 | cls.standard_acl1 = AccessList.objects.create(
26 | name="STANDARD_ACL",
27 | assigned_object=cls.device1,
28 | type=cls.acl_type,
29 | default_action=cls.default_action,
30 | comments="STANDARD_ACL",
31 | )
32 | cls.standard_acl2 = AccessList.objects.create(
33 | name="STANDARD_ACL",
34 | assigned_object=cls.virtual_machine1,
35 | type=cls.acl_type,
36 | default_action=cls.default_action,
37 | comments="STANDARD_ACL",
38 | )
39 |
40 | def test_acl_standard_rule_creation_success(self):
41 | """
42 | Test that ACLStandardRule creation passes validation.
43 | """
44 | created_rule = ACLStandardRule(
45 | access_list=self.standard_acl1,
46 | index=10,
47 | action="permit",
48 | remark="",
49 | source_prefix=None,
50 | description="Created rule with any source prefix",
51 | )
52 | created_rule.full_clean()
53 |
54 | self.assertTrue(isinstance(created_rule, ACLStandardRule), True)
55 | self.assertEqual(created_rule.index, 10)
56 | self.assertEqual(created_rule.action, "permit")
57 | self.assertEqual(created_rule.remark, "")
58 | self.assertEqual(created_rule.source_prefix, None)
59 | self.assertEqual(created_rule.description, "Created rule with any source prefix")
60 | self.assertEqual(isinstance(created_rule.access_list, AccessList), True)
61 | self.assertEqual(created_rule.access_list.type, self.acl_type)
62 |
63 | def test_acl_standard_rule_source_prefix_creation_success(self):
64 | """
65 | Test that ACLStandardRule with source prefix creation passes validation.
66 | """
67 | created_rule = ACLStandardRule(
68 | access_list=self.standard_acl1,
69 | index=20,
70 | action="permit",
71 | remark="",
72 | source_prefix=self.prefix1,
73 | description="Created rule with source prefix",
74 | )
75 | created_rule.full_clean()
76 |
77 | self.assertTrue(isinstance(created_rule, ACLStandardRule), True)
78 | self.assertEqual(created_rule.index, 20)
79 | self.assertEqual(created_rule.action, "permit")
80 | self.assertEqual(created_rule.remark, "")
81 | self.assertEqual(created_rule.source_prefix, self.prefix1)
82 | self.assertEqual(created_rule.description, "Created rule with source prefix")
83 | self.assertEqual(isinstance(created_rule.access_list, AccessList), True)
84 | self.assertEqual(created_rule.access_list.type, self.acl_type)
85 |
86 | def test_acl_standard_rule_remark_creation_success(self):
87 | """
88 | Test that ACLStandardRule with remark creation passes validation.
89 | """
90 | created_rule = ACLStandardRule(
91 | access_list=self.standard_acl1,
92 | index=30,
93 | action="remark",
94 | remark="Test remark",
95 | source_prefix=None,
96 | description="Created rule with remark",
97 | )
98 | created_rule.full_clean()
99 |
100 | self.assertTrue(isinstance(created_rule, ACLStandardRule), True)
101 | self.assertEqual(created_rule.index, 30)
102 | self.assertEqual(created_rule.action, "remark")
103 | self.assertEqual(created_rule.remark, "Test remark")
104 | self.assertEqual(created_rule.source_prefix, None)
105 | self.assertEqual(created_rule.description, "Created rule with remark")
106 | self.assertEqual(isinstance(created_rule.access_list, AccessList), True)
107 | self.assertEqual(created_rule.access_list.type, self.acl_type)
108 |
109 | def test_access_list_extended_to_acl_standard_rule_assignment_fail(self):
110 | """
111 | Test that Extended Access List cannot be assigned to ACLStandardRule.
112 | """
113 | extended_acl1 = AccessList.objects.create(
114 | name="EXTENDED_ACL",
115 | assigned_object=self.device1,
116 | type=ACLTypeChoices.TYPE_EXTENDED,
117 | default_action=self.default_action,
118 | comments="EXTENDED_ACL",
119 | )
120 | standard_rule = ACLStandardRule(
121 | access_list=extended_acl1,
122 | index=30,
123 | action="remark",
124 | remark="Test remark",
125 | source_prefix=None,
126 | description="Created rule with remark",
127 | )
128 | with self.assertRaises(ValidationError):
129 | standard_rule.full_clean()
130 |
131 | def test_duplicate_index_per_acl_fail(self):
132 | """
133 | Test that the rule index must be unique per AccessList.
134 | """
135 | params = {
136 | "access_list": self.standard_acl1,
137 | "index": 10,
138 | "action": "permit",
139 | }
140 | rule_1 = ACLStandardRule(**params)
141 | rule_1.full_clean()
142 | rule_1.save()
143 | rule_2 = ACLStandardRule(**params)
144 | with self.assertRaises(ValidationError):
145 | rule_2.full_clean()
146 |
147 | def test_acl_standard_rule_action_permit_with_remark_fail(self):
148 | """
149 | Test that ACLStandardRule with action 'permit' and remark fails validation.
150 | """
151 | invalid_rule = ACLStandardRule(
152 | access_list=self.standard_acl1,
153 | index=10,
154 | action="permit",
155 | remark="Remark",
156 | source_prefix=None,
157 | description="Invalid rule with action 'permit' and remark",
158 | )
159 | with self.assertRaises(ValidationError):
160 | invalid_rule.full_clean()
161 |
162 | def test_acl_standard_rule_action_remark_with_no_remark_fail(self):
163 | """
164 | Test that ACLStandardRule with action 'remark' and without remark fails validation.
165 | """
166 | invalid_rule = ACLStandardRule(
167 | access_list=self.standard_acl1,
168 | index=10,
169 | action="remark",
170 | remark="",
171 | source_prefix=None,
172 | description="Invalid rule with action 'remark' and without remark",
173 | )
174 | with self.assertRaises(ValidationError):
175 | invalid_rule.full_clean()
176 |
177 | def test_acl_standard_rule_action_remark_with_source_prefix_fail(self):
178 | """
179 | Test that ACLStandardRule with action 'remark' and source prefix fails validation.
180 | """
181 | invalid_rule = ACLStandardRule(
182 | access_list=self.standard_acl1,
183 | index=10,
184 | action="remark",
185 | remark="",
186 | source_prefix=self.prefix1,
187 | description="Invalid rule with action 'remark' and source prefix",
188 | )
189 | with self.assertRaises(ValidationError):
190 | invalid_rule.full_clean()
191 |
192 | def test_valid_acl_rule_action_choices(self):
193 | """
194 | Test ACLStandardRule action choices using VALID choices.
195 | """
196 | valid_acl_rule_action_choices = ["deny", "permit", "remark"]
197 |
198 | for action_choice in valid_acl_rule_action_choices:
199 | valid_acl_rule_action = ACLStandardRule(
200 | access_list=self.standard_acl1,
201 | index=10,
202 | action=action_choice,
203 | remark="Remark" if action_choice == "remark" else None,
204 | description=f"VALID ACL RULE ACTION CHOICES USED: action={action_choice}",
205 | )
206 | valid_acl_rule_action.full_clean()
207 |
208 | def test_invalid_acl_rule_action_choices(self):
209 | """
210 | Test ACLStandardRule action choices using INVALID choices.
211 | """
212 | invalid_acl_rule_action_choice = "both"
213 |
214 | invalid_acl_rule_action = ACLStandardRule(
215 | access_list=self.standard_acl1,
216 | index=10,
217 | action=invalid_acl_rule_action_choice,
218 | description=f"INVALID ACL RULE ACTION CHOICES USED: action={invalid_acl_rule_action_choice}",
219 | )
220 |
221 | with self.assertRaises(ValidationError):
222 | invalid_acl_rule_action.full_clean()
223 |
--------------------------------------------------------------------------------
/netbox_acls/forms/filtersets.py:
--------------------------------------------------------------------------------
1 | """
2 | Defines each django model's GUI filter/search options.
3 | """
4 |
5 | from dcim.models import Device, Interface, Region, Site, SiteGroup, VirtualChassis
6 | from django import forms
7 | from django.utils.translation import gettext_lazy as _
8 | from ipam.models import Prefix
9 | from netbox.forms import NetBoxModelFilterSetForm
10 | from utilities.forms.fields import (
11 | DynamicModelChoiceField,
12 | DynamicModelMultipleChoiceField,
13 | TagFilterField,
14 | )
15 | from utilities.forms.rendering import FieldSet
16 | from utilities.forms.utils import add_blank_choice
17 | from virtualization.models import VirtualMachine, VMInterface
18 |
19 | from ..choices import (
20 | ACLActionChoices,
21 | ACLAssignmentDirectionChoices,
22 | ACLProtocolChoices,
23 | ACLRuleActionChoices,
24 | ACLTypeChoices,
25 | )
26 | from ..models import (
27 | AccessList,
28 | ACLExtendedRule,
29 | ACLInterfaceAssignment,
30 | ACLStandardRule,
31 | )
32 |
33 | __all__ = (
34 | "AccessListFilterForm",
35 | "ACLInterfaceAssignmentFilterForm",
36 | "ACLStandardRuleFilterForm",
37 | "ACLExtendedRuleFilterForm",
38 | )
39 |
40 |
41 | class AccessListFilterForm(NetBoxModelFilterSetForm):
42 | """
43 | GUI filter form to search the django AccessList model.
44 | """
45 |
46 | model = AccessList
47 | fieldsets = (
48 | FieldSet("q", "tag", name=None),
49 | FieldSet("type", "default_action", name=_("ACL Details")),
50 | FieldSet("region_id", "site_group_id", "site_id", "device_id", name=_("Device Details")),
51 | FieldSet("virtual_chassis_id", name=_("Virtual Chassis Details")),
52 | FieldSet("virtual_machine_id", name=_("Virtual Machine Details")),
53 | )
54 |
55 | # ACL
56 | type = forms.ChoiceField(
57 | choices=add_blank_choice(ACLTypeChoices),
58 | required=False,
59 | )
60 | default_action = forms.ChoiceField(
61 | choices=add_blank_choice(ACLActionChoices),
62 | required=False,
63 | label=_("Default Action"),
64 | )
65 |
66 | # Device selector
67 | region_id = DynamicModelChoiceField(
68 | queryset=Region.objects.all(),
69 | required=False,
70 | label=_("Region"),
71 | )
72 | site_group_id = DynamicModelChoiceField(
73 | queryset=SiteGroup.objects.all(),
74 | required=False,
75 | label=_("Site Group"),
76 | )
77 | site_id = DynamicModelChoiceField(
78 | queryset=Site.objects.all(),
79 | required=False,
80 | query_params={
81 | "region_id": "$region_id",
82 | "group_id": "$site_group_id",
83 | },
84 | label=_("Site"),
85 | )
86 | device_id = DynamicModelChoiceField(
87 | queryset=Device.objects.all(),
88 | query_params={
89 | "region_id": "$region_id",
90 | "group_id": "$site_group_id",
91 | "site_id": "$site_id",
92 | },
93 | required=False,
94 | label=_("Device"),
95 | )
96 |
97 | # Virtual Chassis selector
98 | virtual_chassis_id = DynamicModelChoiceField(
99 | queryset=VirtualChassis.objects.all(),
100 | required=False,
101 | label=_("Virtual Chassis"),
102 | )
103 |
104 | # Virtual Machine selector
105 | virtual_machine_id = DynamicModelChoiceField(
106 | queryset=VirtualMachine.objects.all(),
107 | required=False,
108 | label=_("Virtual Machine"),
109 | )
110 |
111 | # Tag selector
112 | tag = TagFilterField(model)
113 |
114 |
115 | class ACLInterfaceAssignmentFilterForm(NetBoxModelFilterSetForm):
116 | """
117 | GUI filter form to search the django AccessList model.
118 | """
119 |
120 | model = ACLInterfaceAssignment
121 | fieldsets = (
122 | FieldSet("q", "tag", name=None),
123 | FieldSet("access_list_id", "direction", name=_("ACL Details")),
124 | FieldSet("region_id", "site_group_id", "site_id", "device_id", "interface_id", name=_("Device Details")),
125 | FieldSet("virtual_machine_id", "vminterface_id", name=_("Virtual Machine Details")),
126 | )
127 |
128 | # ACL selector
129 | access_list_id = DynamicModelChoiceField(
130 | queryset=AccessList.objects.all(),
131 | required=False,
132 | label=_("Access List"),
133 | )
134 | direction = forms.ChoiceField(
135 | choices=add_blank_choice(ACLAssignmentDirectionChoices),
136 | required=False,
137 | label=_("Direction"),
138 | )
139 |
140 | # Device Interface selector
141 | region_id = DynamicModelChoiceField(
142 | queryset=Region.objects.all(),
143 | required=False,
144 | label=_("Region"),
145 | )
146 | site_group_id = DynamicModelChoiceField(
147 | queryset=SiteGroup.objects.all(),
148 | required=False,
149 | label=_("Site Group"),
150 | )
151 | site_id = DynamicModelChoiceField(
152 | queryset=Site.objects.all(),
153 | required=False,
154 | query_params={
155 | "region_id": "$region_id",
156 | "group_id": "$site_group_id",
157 | },
158 | label=_("Site"),
159 | )
160 | device_id = DynamicModelChoiceField(
161 | queryset=Device.objects.all(),
162 | query_params={
163 | "region_id": "$region_id",
164 | "group_id": "$site_group_id",
165 | "site_id": "$site_id",
166 | },
167 | required=False,
168 | label=_("Device"),
169 | )
170 | interface_id = DynamicModelChoiceField(
171 | queryset=Interface.objects.all(),
172 | required=False,
173 | query_params={
174 | "device_id": "$device_id",
175 | },
176 | label=_("Device Interface"),
177 | )
178 |
179 | # Virtual Machine Interface selector
180 | virtual_machine_id = DynamicModelChoiceField(
181 | queryset=VirtualMachine.objects.all(),
182 | required=False,
183 | label=_("Virtual Machine"),
184 | )
185 | vminterface_id = DynamicModelChoiceField(
186 | queryset=VMInterface.objects.all(),
187 | required=False,
188 | query_params={
189 | "virtual_machine_id": "$virtual_machine_id",
190 | },
191 | label=_("VM Interface"),
192 | )
193 |
194 | # Tag selector
195 | tag = TagFilterField(model)
196 |
197 |
198 | class ACLStandardRuleFilterForm(NetBoxModelFilterSetForm):
199 | """
200 | GUI filter form to search the django ACLStandardRule model.
201 | """
202 |
203 | model = ACLStandardRule
204 | fieldsets = (
205 | FieldSet("q", "tag", name=None),
206 | FieldSet("access_list_id", "index", "action", name=_("ACL Details")),
207 | FieldSet("source_prefix_id", name=_("Source Details")),
208 | )
209 |
210 | access_list_id = DynamicModelMultipleChoiceField(
211 | queryset=AccessList.objects.all(),
212 | query_params={
213 | "type": ACLTypeChoices.TYPE_STANDARD,
214 | },
215 | required=False,
216 | label=_("Access List"),
217 | )
218 | index = forms.IntegerField(
219 | required=False,
220 | label=_("Index"),
221 | )
222 | action = forms.ChoiceField(
223 | choices=add_blank_choice(ACLRuleActionChoices),
224 | required=False,
225 | label=_("Action"),
226 | )
227 |
228 | # Source selectors
229 | source_prefix_id = DynamicModelMultipleChoiceField(
230 | queryset=Prefix.objects.all(),
231 | required=False,
232 | label=_("Source Prefix"),
233 | )
234 |
235 | # Tag selector
236 | tag = TagFilterField(model)
237 |
238 |
239 | class ACLExtendedRuleFilterForm(NetBoxModelFilterSetForm):
240 | """
241 | GUI filter form to search the django ACLExtendedRule model.
242 | """
243 |
244 | model = ACLExtendedRule
245 | fieldsets = (
246 | FieldSet("q", "tag", name=None),
247 | FieldSet("access_list_id", "index", "action", "protocol", name=_("ACL Details")),
248 | FieldSet("source_prefix_id", name=_("Source Details")),
249 | FieldSet("destination_prefix_id", name=_("Destination Details")),
250 | )
251 |
252 | access_list_id = DynamicModelMultipleChoiceField(
253 | queryset=AccessList.objects.all(),
254 | query_params={
255 | "type": ACLTypeChoices.TYPE_EXTENDED,
256 | },
257 | required=False,
258 | label=_("Access List"),
259 | )
260 | index = forms.IntegerField(
261 | required=False,
262 | label=_("Index"),
263 | )
264 | action = forms.ChoiceField(
265 | choices=add_blank_choice(ACLRuleActionChoices),
266 | required=False,
267 | label=_("Action"),
268 | )
269 | protocol = forms.ChoiceField(
270 | choices=add_blank_choice(ACLProtocolChoices),
271 | required=False,
272 | label=_("Protocol"),
273 | )
274 |
275 | # Source selectors
276 | source_prefix_id = DynamicModelMultipleChoiceField(
277 | queryset=Prefix.objects.all(),
278 | required=False,
279 | label=_("Source Prefix"),
280 | )
281 |
282 | # Destination selectors
283 | destination_prefix_id = DynamicModelMultipleChoiceField(
284 | queryset=Prefix.objects.all(),
285 | required=False,
286 | label=_("Destination Prefix"),
287 | )
288 |
289 | # Tag selector
290 | tag = TagFilterField(model)
291 |
--------------------------------------------------------------------------------
/netbox_acls/models/access_list_rules.py:
--------------------------------------------------------------------------------
1 | """
2 | Define the django models for this plugin.
3 | """
4 |
5 | from django.contrib.postgres.fields import ArrayField
6 | from django.core.exceptions import ValidationError
7 | from django.db import models
8 | from django.urls import reverse
9 | from django.utils.translation import gettext_lazy as _
10 | from netbox.models import NetBoxModel
11 |
12 | from ..choices import ACLProtocolChoices, ACLRuleActionChoices, ACLTypeChoices
13 | from .access_lists import AccessList
14 |
15 | __all__ = (
16 | "ACLRule",
17 | "ACLStandardRule",
18 | "ACLExtendedRule",
19 | )
20 |
21 | # Error message when the action is 'remark', but no remark is provided.
22 | ERROR_MESSAGE_NO_REMARK = _("When the action is 'remark', a remark is required.")
23 |
24 | # Error message when the action is 'remark', but the source_prefix is set.
25 | ERROR_MESSAGE_ACTION_REMARK_SOURCE_PREFIX_SET = _("When the action is 'remark', the Source Prefix must not be set.")
26 |
27 | # Error message when the action is 'remark', but the source_ports are set.
28 | ERROR_MESSAGE_ACTION_REMARK_SOURCE_PORTS_SET = _("When the action is 'remark', Source Ports must not be set.")
29 |
30 | # Error message when the action is 'remark', but the destination_prefix is set.
31 | ERROR_MESSAGE_ACTION_REMARK_DESTINATION_PREFIX_SET = _(
32 | "When the action is 'remark', the Destination Prefix must not be set."
33 | )
34 |
35 | # Error message when the action is 'remark', but the destination_ports are set.
36 | ERROR_MESSAGE_ACTION_REMARK_DESTINATION_PORTS_SET = _("When the action is 'remark', Destination Ports must not be set.")
37 |
38 | # Error message when the action is 'remark', but the protocol is set.
39 | ERROR_MESSAGE_ACTION_REMARK_PROTOCOL_SET = _("When the action is 'remark', Protocol must not be set.")
40 |
41 | # Error message when a remark is provided, but the action is not set to 'remark'.
42 | ERROR_MESSAGE_REMARK_WITHOUT_ACTION_REMARK = _("A remark cannot be set unless the action is 'remark'.")
43 |
44 |
45 | class ACLRule(NetBoxModel):
46 | """
47 | Abstract model for ACL Rules.
48 | Inherited by both ACLStandardRule and ACLExtendedRule.
49 | """
50 |
51 | access_list = models.ForeignKey(
52 | to=AccessList,
53 | on_delete=models.CASCADE,
54 | related_name="rules",
55 | verbose_name=_("Access List"),
56 | )
57 | index = models.PositiveIntegerField()
58 | remark = models.CharField(
59 | verbose_name=_("Remark"),
60 | max_length=500,
61 | blank=True,
62 | )
63 | description = models.CharField(
64 | verbose_name=_("Description"),
65 | max_length=500,
66 | blank=True,
67 | )
68 | action = models.CharField(
69 | verbose_name=_("Action"),
70 | max_length=30,
71 | choices=ACLRuleActionChoices,
72 | )
73 | source_prefix = models.ForeignKey(
74 | to="ipam.prefix",
75 | on_delete=models.PROTECT,
76 | related_name="+",
77 | verbose_name=_("Source Prefix"),
78 | blank=True,
79 | null=True,
80 | )
81 |
82 | clone_fields = ("access_list", "action", "source_prefix")
83 | prerequisite_models = ("netbox_acls.AccessList",)
84 |
85 | class Meta:
86 | """
87 | Define the common model properties:
88 | - as an abstract model
89 | - ordering
90 | - unique together
91 | """
92 |
93 | abstract = True
94 | ordering = ["access_list", "index"]
95 | unique_together = ["access_list", "index"]
96 |
97 | def __str__(self):
98 | return f"{self.access_list}: Rule {self.index}"
99 |
100 | def get_absolute_url(self):
101 | """
102 | The method is a Django convention; although not strictly required,
103 | it conveniently returns the absolute URL for any particular object.
104 | """
105 | return reverse(
106 | f"plugins:{self._meta.app_label}:{self._meta.model_name}",
107 | args=[self.pk],
108 | )
109 |
110 | def get_action_color(self):
111 | return ACLRuleActionChoices.colors.get(self.action)
112 |
113 |
114 | class ACLStandardRule(ACLRule):
115 | """
116 | Inherits ACLRule.
117 | """
118 |
119 | access_list = models.ForeignKey(
120 | to=AccessList,
121 | on_delete=models.CASCADE,
122 | related_name="aclstandardrules",
123 | limit_choices_to={"type": ACLTypeChoices.TYPE_STANDARD},
124 | verbose_name=_("Standard Access List"),
125 | )
126 |
127 | class Meta(ACLRule.Meta):
128 | """
129 | Define the model properties adding to or overriding the inherited class:
130 | - default_related_name for any FK relationships
131 | - verbose name (for displaying in the GUI)
132 | - verbose name plural (for displaying in the GUI)
133 | """
134 |
135 | verbose_name = _("ACL Standard Rule")
136 | verbose_name_plural = _("ACL Standard Rules")
137 |
138 | def clean(self):
139 | """
140 | Validate the ACL Standard Rule inputs.
141 |
142 | If the action is 'remark', then the remark field must be provided (non-empty),
143 | and the source_prefix field must be empty.
144 | Conversely, if the remark field is provided, the action must be set to 'remark'.
145 | """
146 |
147 | super().clean()
148 | errors = {}
149 |
150 | # Validate that only the remark field is filled
151 | if self.action == ACLRuleActionChoices.ACTION_REMARK:
152 | if not self.remark:
153 | errors["remark"] = ERROR_MESSAGE_NO_REMARK
154 | if self.source_prefix:
155 | errors["source_prefix"] = ERROR_MESSAGE_ACTION_REMARK_SOURCE_PREFIX_SET
156 | # Validate that the action is "remark", when the remark field is provided
157 | elif self.remark:
158 | errors["remark"] = ERROR_MESSAGE_REMARK_WITHOUT_ACTION_REMARK
159 |
160 | if errors:
161 | raise ValidationError(errors)
162 |
163 |
164 | class ACLExtendedRule(ACLRule):
165 | """
166 | Inherits ACLRule.
167 | Add ACLExtendedRule specific fields: source_ports, destination_prefix, destination_ports, and protocol
168 | """
169 |
170 | access_list = models.ForeignKey(
171 | to=AccessList,
172 | on_delete=models.CASCADE,
173 | related_name="aclextendedrules",
174 | limit_choices_to={"type": "extended"},
175 | verbose_name=_("Extended Access List"),
176 | )
177 | source_ports = ArrayField(
178 | base_field=models.PositiveIntegerField(),
179 | verbose_name=_("Source Ports"),
180 | blank=True,
181 | null=True,
182 | )
183 | destination_prefix = models.ForeignKey(
184 | to="ipam.prefix",
185 | on_delete=models.PROTECT,
186 | related_name="+",
187 | verbose_name=_("Destination Prefix"),
188 | blank=True,
189 | null=True,
190 | )
191 | destination_ports = ArrayField(
192 | base_field=models.PositiveIntegerField(),
193 | verbose_name=_("Destination Ports"),
194 | blank=True,
195 | null=True,
196 | )
197 | protocol = models.CharField(
198 | verbose_name=_("Protocol"),
199 | max_length=30,
200 | choices=ACLProtocolChoices,
201 | blank=True,
202 | )
203 |
204 | clone_fields = (
205 | "access_list",
206 | "action",
207 | "source_prefix",
208 | "source_ports",
209 | "destination_prefix",
210 | "destination_ports",
211 | "protocol",
212 | )
213 |
214 | class Meta(ACLRule.Meta):
215 | """
216 | Define the model properties adding to or overriding the inherited class:
217 | - default_related_name for any FK relationships
218 | - verbose name (for displaying in the GUI)
219 | - verbose name plural (for displaying in the GUI)
220 | """
221 |
222 | verbose_name = _("ACL Extended Rule")
223 | verbose_name_plural = _("ACL Extended Rules")
224 |
225 | def clean(self):
226 | """
227 | Validate the ACL Extended Rule inputs.
228 |
229 | When the action is 'remark', the remark field must be provided (non-empty),
230 | and the following fields must be empty:
231 | - source_prefix
232 | - source_ports
233 | - destination_prefix
234 | - destination_ports
235 | - protocol
236 |
237 | Conversely, if a remark is provided, the action must be set to 'remark'.
238 | """
239 | super().clean()
240 | errors = {}
241 |
242 | # Validate that only the remark field is filled
243 | if self.action == ACLRuleActionChoices.ACTION_REMARK:
244 | if not self.remark:
245 | errors["remark"] = ERROR_MESSAGE_NO_REMARK
246 | if self.source_prefix:
247 | errors["source_prefix"] = ERROR_MESSAGE_ACTION_REMARK_SOURCE_PREFIX_SET
248 | if self.source_ports:
249 | errors["source_ports"] = ERROR_MESSAGE_ACTION_REMARK_SOURCE_PORTS_SET
250 | if self.destination_prefix:
251 | errors["destination_prefix"] = ERROR_MESSAGE_ACTION_REMARK_DESTINATION_PREFIX_SET
252 | if self.destination_ports:
253 | errors["destination_ports"] = ERROR_MESSAGE_ACTION_REMARK_DESTINATION_PORTS_SET
254 | if self.protocol:
255 | errors["protocol"] = ERROR_MESSAGE_ACTION_REMARK_PROTOCOL_SET
256 | # Validate that the action is "remark", when the remark field is provided
257 | elif self.remark:
258 | errors["remark"] = ERROR_MESSAGE_REMARK_WITHOUT_ACTION_REMARK
259 |
260 | if errors:
261 | raise ValidationError(errors)
262 |
263 | def get_protocol_color(self):
264 | return ACLProtocolChoices.colors.get(self.protocol)
265 |
--------------------------------------------------------------------------------
/netbox_acls/tests/api/test_access_lists.py:
--------------------------------------------------------------------------------
1 | from dcim.choices import InterfaceTypeChoices
2 | from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
3 | from django.contrib.contenttypes.models import ContentType
4 | from utilities.testing import APIViewTestCases
5 | from virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
6 |
7 | from netbox_acls.choices import (
8 | ACLActionChoices,
9 | ACLAssignmentDirectionChoices,
10 | ACLTypeChoices,
11 | )
12 | from netbox_acls.models import AccessList, ACLInterfaceAssignment
13 |
14 |
15 | class AccessListAPIViewTestCase(APIViewTestCases.APIViewTestCase):
16 | """
17 | API view test case for AccessList.
18 | """
19 |
20 | model = AccessList
21 | view_namespace = "plugins-api:netbox_acls"
22 | brief_fields = ["display", "id", "name", "url"]
23 | user_permissions = (
24 | "dcim.view_site",
25 | "dcim.view_devicetype",
26 | "dcim.view_device",
27 | "virtualization.view_cluster",
28 | "virtualization.view_clustergroup",
29 | "virtualization.view_clustertype",
30 | "virtualization.view_virtualmachine",
31 | )
32 |
33 | @classmethod
34 | def setUpTestData(cls):
35 | """Set up Access List for API view testing."""
36 | site = Site.objects.create(
37 | name="Site 1",
38 | slug="site-1",
39 | )
40 |
41 | # Device
42 | manufacturer = Manufacturer.objects.create(
43 | name="Manufacturer 1",
44 | slug="manufacturer-1",
45 | )
46 | device_type = DeviceType.objects.create(
47 | manufacturer=manufacturer,
48 | model="Device Type 1",
49 | )
50 | device_role = DeviceRole.objects.create(
51 | name="Device Role 1",
52 | slug="device-role-1",
53 | )
54 | device = Device.objects.create(
55 | name="Device 1",
56 | site=site,
57 | device_type=device_type,
58 | role=device_role,
59 | )
60 |
61 | # Virtual Machine
62 | cluster_type = ClusterType.objects.create(
63 | name="Cluster Type 1",
64 | slug="cluster-type-1",
65 | )
66 | cluster = Cluster.objects.create(
67 | name="Cluster 1",
68 | type=cluster_type,
69 | )
70 | virtual_machine = VirtualMachine.objects.create(
71 | name="VM 1",
72 | cluster=cluster,
73 | )
74 |
75 | access_lists = (
76 | AccessList(
77 | name="testacl1",
78 | assigned_object_type=ContentType.objects.get_for_model(Device),
79 | assigned_object_id=device.id,
80 | type=ACLTypeChoices.TYPE_STANDARD,
81 | default_action=ACLActionChoices.ACTION_DENY,
82 | ),
83 | AccessList(
84 | name="testacl2",
85 | assigned_object=device,
86 | type=ACLTypeChoices.TYPE_EXTENDED,
87 | default_action=ACLActionChoices.ACTION_PERMIT,
88 | ),
89 | AccessList(
90 | name="testacl3",
91 | assigned_object_type=ContentType.objects.get_for_model(VirtualMachine),
92 | assigned_object_id=virtual_machine.id,
93 | type=ACLTypeChoices.TYPE_EXTENDED,
94 | default_action=ACLActionChoices.ACTION_DENY,
95 | ),
96 | )
97 | AccessList.objects.bulk_create(access_lists)
98 |
99 | cls.create_data = [
100 | {
101 | "name": "testacl4",
102 | "assigned_object_type": "dcim.device",
103 | "assigned_object_id": device.id,
104 | "type": ACLTypeChoices.TYPE_STANDARD,
105 | "default_action": ACLActionChoices.ACTION_DENY,
106 | },
107 | {
108 | "name": "testacl5",
109 | "assigned_object_type": "dcim.device",
110 | "assigned_object_id": device.id,
111 | "type": ACLTypeChoices.TYPE_EXTENDED,
112 | "default_action": ACLActionChoices.ACTION_DENY,
113 | },
114 | {
115 | "name": "testacl6",
116 | "assigned_object_type": "virtualization.virtualmachine",
117 | "assigned_object_id": virtual_machine.id,
118 | "type": ACLTypeChoices.TYPE_STANDARD,
119 | "default_action": ACLActionChoices.ACTION_PERMIT,
120 | },
121 | ]
122 | cls.bulk_update_data = {
123 | "comments": "Rule bulk update",
124 | }
125 |
126 |
127 | class ACLInterfaceAssignmentAPIViewTestCase(APIViewTestCases.APIViewTestCase):
128 | """
129 | API view test case for ACLInterfaceAssignment.
130 | """
131 |
132 | model = ACLInterfaceAssignment
133 | view_namespace = "plugins-api:netbox_acls"
134 | brief_fields = ["access_list", "display", "id", "url"]
135 | user_permissions = (
136 | "dcim.view_site",
137 | "dcim.view_devicetype",
138 | "dcim.view_device",
139 | "dcim.view_interface",
140 | "virtualization.view_cluster",
141 | "virtualization.view_clustergroup",
142 | "virtualization.view_clustertype",
143 | "virtualization.view_virtualmachine",
144 | "virtualization.view_vminterface",
145 | "netbox_acls.view_accesslist",
146 | )
147 |
148 | @classmethod
149 | def setUpTestData(cls):
150 | """Set up ACL Interface Assignment for API view testing."""
151 | site = Site.objects.create(
152 | name="Site 1",
153 | slug="site-1",
154 | )
155 |
156 | # Device
157 | manufacturer = Manufacturer.objects.create(
158 | name="Manufacturer 1",
159 | slug="manufacturer-1",
160 | )
161 | device_type = DeviceType.objects.create(
162 | manufacturer=manufacturer,
163 | model="Device Type 1",
164 | )
165 | device_role = DeviceRole.objects.create(
166 | name="Device Role 1",
167 | slug="device-role-1",
168 | )
169 | device = Device.objects.create(
170 | name="Device 1",
171 | site=site,
172 | device_type=device_type,
173 | role=device_role,
174 | )
175 | device_interface1 = device.interfaces.create(
176 | name="DeviceInterface1",
177 | device=device,
178 | type=InterfaceTypeChoices.TYPE_1GE_FIXED,
179 | )
180 | device_interface2 = device.interfaces.create(
181 | name="DeviceInterface2",
182 | device=device,
183 | type=InterfaceTypeChoices.TYPE_1GE_FIXED,
184 | )
185 | device_interface3 = device.interfaces.create(
186 | name="DeviceInterface3",
187 | device=device,
188 | type=InterfaceTypeChoices.TYPE_1GE_FIXED,
189 | )
190 |
191 | # Virtual Machine
192 | cluster_type = ClusterType.objects.create(
193 | name="Cluster Type 1",
194 | slug="cluster-type-1",
195 | )
196 | cluster = Cluster.objects.create(
197 | name="Cluster 1",
198 | type=cluster_type,
199 | )
200 | virtual_machine = VirtualMachine.objects.create(
201 | name="VM 1",
202 | cluster=cluster,
203 | )
204 | virtual_machine_interface1 = virtual_machine.interfaces.create(
205 | name="eth0",
206 | virtual_machine=virtual_machine,
207 | )
208 | virtual_machine_interface2 = virtual_machine.interfaces.create(
209 | name="eth1",
210 | virtual_machine=virtual_machine,
211 | )
212 | virtual_machine_interface3 = virtual_machine.interfaces.create(
213 | name="eth2",
214 | virtual_machine=virtual_machine,
215 | )
216 |
217 | # AccessList
218 | access_list_device = AccessList.objects.create(
219 | name="testacl1",
220 | assigned_object=device,
221 | type=ACLTypeChoices.TYPE_STANDARD,
222 | default_action=ACLActionChoices.ACTION_DENY,
223 | )
224 | access_list_vm = AccessList.objects.create(
225 | name="testacl2",
226 | assigned_object=virtual_machine,
227 | type=ACLTypeChoices.TYPE_EXTENDED,
228 | default_action=ACLActionChoices.ACTION_PERMIT,
229 | )
230 |
231 | acl_interface_assignments = (
232 | ACLInterfaceAssignment(
233 | access_list=access_list_device,
234 | direction=ACLAssignmentDirectionChoices.DIRECTION_INGRESS,
235 | assigned_object_type=ContentType.objects.get_for_model(Interface),
236 | assigned_object_id=device_interface1.id,
237 | ),
238 | ACLInterfaceAssignment(
239 | access_list=access_list_device,
240 | direction=ACLAssignmentDirectionChoices.DIRECTION_EGRESS,
241 | assigned_object=device_interface2,
242 | ),
243 | ACLInterfaceAssignment(
244 | access_list=access_list_vm,
245 | direction=ACLAssignmentDirectionChoices.DIRECTION_EGRESS,
246 | assigned_object_type=ContentType.objects.get_for_model(VMInterface),
247 | assigned_object_id=virtual_machine_interface1.id,
248 | ),
249 | )
250 | ACLInterfaceAssignment.objects.bulk_create(acl_interface_assignments)
251 |
252 | cls.create_data = [
253 | {
254 | "access_list": access_list_device.id,
255 | "assigned_object_type": "dcim.interface",
256 | "assigned_object_id": device_interface3.id,
257 | "direction": ACLAssignmentDirectionChoices.DIRECTION_EGRESS,
258 | },
259 | {
260 | "access_list": access_list_vm.id,
261 | "assigned_object_type": "virtualization.vminterface",
262 | "assigned_object_id": virtual_machine_interface2.id,
263 | "direction": ACLAssignmentDirectionChoices.DIRECTION_INGRESS,
264 | },
265 | {
266 | "access_list": access_list_vm.id,
267 | "assigned_object_type": "virtualization.vminterface",
268 | "assigned_object_id": virtual_machine_interface3.id,
269 | "direction": ACLAssignmentDirectionChoices.DIRECTION_EGRESS,
270 | },
271 | ]
272 |
--------------------------------------------------------------------------------
/netbox_acls/filtersets.py:
--------------------------------------------------------------------------------
1 | """
2 | Filters enable users to request only a specific subset of objects matching a query;
3 | when filtering the site list by status or region, for instance.
4 | """
5 |
6 | import django_filters
7 | from dcim.models import Device, Interface, Region, Site, SiteGroup, VirtualChassis
8 | from django.db.models import Q
9 | from django.utils.translation import gettext_lazy as _
10 | from ipam.models import Prefix
11 | from netbox.filtersets import NetBoxModelFilterSet
12 | from virtualization.models import VirtualMachine, VMInterface
13 |
14 | from .choices import ACLTypeChoices
15 | from .models import AccessList, ACLExtendedRule, ACLInterfaceAssignment, ACLStandardRule
16 |
17 | __all__ = (
18 | "AccessListFilterSet",
19 | "ACLStandardRuleFilterSet",
20 | "ACLInterfaceAssignmentFilterSet",
21 | "ACLExtendedRuleFilterSet",
22 | )
23 |
24 |
25 | class AccessListFilterSet(NetBoxModelFilterSet):
26 | """
27 | Define the filter set for the django model AccessList.
28 | """
29 |
30 | region = django_filters.ModelMultipleChoiceFilter(
31 | field_name="device__site__region",
32 | queryset=Region.objects.all(),
33 | to_field_name="id",
34 | label="Region",
35 | )
36 | site_group = django_filters.ModelMultipleChoiceFilter(
37 | field_name="device__site__group",
38 | queryset=SiteGroup.objects.all(),
39 | to_field_name="id",
40 | label="Site Group",
41 | )
42 | site = django_filters.ModelMultipleChoiceFilter(
43 | field_name="device__site",
44 | queryset=Site.objects.all(),
45 | to_field_name="id",
46 | label="Site",
47 | )
48 | device = django_filters.ModelMultipleChoiceFilter(
49 | field_name="device__name",
50 | queryset=Device.objects.all(),
51 | to_field_name="name",
52 | label="Device (name)",
53 | )
54 | device_id = django_filters.ModelMultipleChoiceFilter(
55 | field_name="device",
56 | queryset=Device.objects.all(),
57 | label="Device (ID)",
58 | )
59 | virtual_chassis = django_filters.ModelMultipleChoiceFilter(
60 | field_name="virtual_chassis__name",
61 | queryset=VirtualChassis.objects.all(),
62 | to_field_name="name",
63 | label="Virtual Chassis (name)",
64 | )
65 | virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
66 | field_name="virtual_chassis",
67 | queryset=VirtualChassis.objects.all(),
68 | label="Virtual Chassis (ID)",
69 | )
70 | virtual_machine = django_filters.ModelMultipleChoiceFilter(
71 | field_name="virtual_machine__name",
72 | queryset=VirtualMachine.objects.all(),
73 | to_field_name="name",
74 | label="Virtual Machine (name)",
75 | )
76 | virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
77 | field_name="virtual_machine",
78 | queryset=VirtualMachine.objects.all(),
79 | label="Virtual machine (ID)",
80 | )
81 |
82 | class Meta:
83 | """
84 | Associates the django model AccessList & fields to the filter set.
85 | """
86 |
87 | model = AccessList
88 | fields = (
89 | "id",
90 | "name",
91 | "device",
92 | "device_id",
93 | "virtual_chassis",
94 | "virtual_chassis_id",
95 | "virtual_machine",
96 | "virtual_machine_id",
97 | "type",
98 | "default_action",
99 | "comments",
100 | "site",
101 | "site_group",
102 | "region",
103 | )
104 |
105 | def search(self, queryset, name, value):
106 | """
107 | Override the default search behavior for the django model.
108 | """
109 | query = (
110 | Q(name__icontains=value)
111 | | Q(device__name__icontains=value)
112 | | Q(virtual_chassis__name__icontains=value)
113 | | Q(virtual_machine__name__icontains=value)
114 | | Q(type__icontains=value)
115 | | Q(default_action__icontains=value)
116 | | Q(comments__icontains=value)
117 | )
118 | return queryset.filter(query)
119 |
120 |
121 | class ACLInterfaceAssignmentFilterSet(NetBoxModelFilterSet):
122 | """
123 | Define the filter set for the django model ACLInterfaceAssignment.
124 | """
125 |
126 | access_list = django_filters.ModelMultipleChoiceFilter(
127 | queryset=AccessList.objects.all(),
128 | to_field_name="name",
129 | label=_("Access List (name)"),
130 | )
131 | access_list_id = django_filters.ModelMultipleChoiceFilter(
132 | queryset=AccessList.objects.all(),
133 | to_field_name="id",
134 | label=_("Access List (ID)"),
135 | )
136 | interface = django_filters.ModelMultipleChoiceFilter(
137 | field_name="interface__name",
138 | queryset=Interface.objects.all(),
139 | to_field_name="name",
140 | label="Interface (name)",
141 | )
142 | interface_id = django_filters.ModelMultipleChoiceFilter(
143 | field_name="interface",
144 | queryset=Interface.objects.all(),
145 | label="Interface (ID)",
146 | )
147 | vminterface = django_filters.ModelMultipleChoiceFilter(
148 | field_name="vminterface__name",
149 | queryset=VMInterface.objects.all(),
150 | to_field_name="name",
151 | label="VM Interface (name)",
152 | )
153 | vminterface_id = django_filters.ModelMultipleChoiceFilter(
154 | field_name="vminterface",
155 | queryset=VMInterface.objects.all(),
156 | label="VM Interface (ID)",
157 | )
158 |
159 | class Meta:
160 | """
161 | Associates the django model ACLInterfaceAssignment & fields to the filter set.
162 | """
163 |
164 | model = ACLInterfaceAssignment
165 | fields = (
166 | "id",
167 | "access_list",
168 | "direction",
169 | "interface",
170 | "interface_id",
171 | "vminterface",
172 | "vminterface_id",
173 | )
174 |
175 | def search(self, queryset, name, value):
176 | """
177 | Override the default search behavior for the django model.
178 | """
179 | query = (
180 | Q(access_list__name__icontains=value)
181 | | Q(direction__icontains=value)
182 | | Q(interface__name__icontains=value)
183 | | Q(vminterface__name__icontains=value)
184 | )
185 | return queryset.filter(query)
186 |
187 |
188 | class ACLStandardRuleFilterSet(NetBoxModelFilterSet):
189 | """
190 | Define the filter set for the django model ACLStandardRule.
191 | """
192 |
193 | # Access List
194 | access_list = django_filters.ModelMultipleChoiceFilter(
195 | queryset=AccessList.objects.all(),
196 | to_field_name="name",
197 | label=_("Access List (name)"),
198 | )
199 | access_list_id = django_filters.ModelMultipleChoiceFilter(
200 | queryset=AccessList.objects.all(),
201 | to_field_name="id",
202 | label=_("Access List (ID)"),
203 | )
204 |
205 | # Source
206 | source_prefix = django_filters.ModelMultipleChoiceFilter(
207 | field_name="source_prefix",
208 | queryset=Prefix.objects.all(),
209 | to_field_name="name",
210 | label=_("Source Prefix (name)"),
211 | )
212 | source_prefix_id = django_filters.ModelMultipleChoiceFilter(
213 | field_name="source_prefix",
214 | queryset=Prefix.objects.all(),
215 | to_field_name="id",
216 | label=_("Source Prefix (ID)"),
217 | )
218 |
219 | class Meta:
220 | """
221 | Associates the django model ACLStandardRule & fields to the filter set.
222 | """
223 |
224 | model = ACLStandardRule
225 | fields = ("id", "access_list", "index", "action")
226 |
227 | def search(self, queryset, name, value):
228 | """
229 | Override the default search behavior for the django model.
230 | """
231 | query = (
232 | Q(access_list__name__icontains=value)
233 | | Q(index__icontains=value)
234 | | Q(action__icontains=value)
235 | )
236 | return queryset.filter(query)
237 |
238 |
239 | class ACLExtendedRuleFilterSet(NetBoxModelFilterSet):
240 | """
241 | Define the filter set for the django model ACLExtendedRule.
242 | """
243 |
244 | # Access List
245 | access_list = django_filters.ModelMultipleChoiceFilter(
246 | queryset=AccessList.objects.filter(type=ACLTypeChoices.TYPE_EXTENDED),
247 | to_field_name="name",
248 | label=_("Access List (name)"),
249 | )
250 | access_list_id = django_filters.ModelMultipleChoiceFilter(
251 | queryset=AccessList.objects.filter(type=ACLTypeChoices.TYPE_EXTENDED),
252 | to_field_name="id",
253 | label=_("Access List (ID)"),
254 | )
255 |
256 | # Source
257 | source_prefix = django_filters.ModelMultipleChoiceFilter(
258 | field_name="source_prefix",
259 | queryset=Prefix.objects.all(),
260 | to_field_name="name",
261 | label=_("Source Prefix (name)"),
262 | )
263 | source_prefix_id = django_filters.ModelMultipleChoiceFilter(
264 | field_name="source_prefix",
265 | queryset=Prefix.objects.all(),
266 | to_field_name="id",
267 | label=_("Source Prefix (ID)"),
268 | )
269 |
270 | # Destination
271 | destination_prefix = django_filters.ModelMultipleChoiceFilter(
272 | field_name="destination_prefix",
273 | queryset=Prefix.objects.all(),
274 | to_field_name="name",
275 | label=_("Destination Prefix (name)"),
276 | )
277 | destination_prefix_id = django_filters.ModelMultipleChoiceFilter(
278 | field_name="destination_prefix",
279 | queryset=Prefix.objects.all(),
280 | to_field_name="id",
281 | label=_("Destination Prefix (ID)"),
282 | )
283 |
284 | class Meta:
285 | """
286 | Associates the django model ACLExtendedRule & fields to the filter set.
287 | """
288 |
289 | model = ACLExtendedRule
290 | fields = ("id", "access_list", "index", "action", "protocol")
291 |
292 | def search(self, queryset, name, value):
293 | """
294 | Override the default search behavior for the django model.
295 | """
296 | query = (
297 | Q(access_list__name__icontains=value)
298 | | Q(index__icontains=value)
299 | | Q(action__icontains=value)
300 | | Q(protocol__icontains=value)
301 | )
302 | return queryset.filter(query)
303 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/netbox_acls/tests/api/test_access_list_rules.py:
--------------------------------------------------------------------------------
1 | from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
2 | from ipam.models import Prefix
3 | from utilities.testing import APIViewTestCases
4 | from virtualization.models import Cluster, ClusterType, VirtualMachine
5 |
6 | from netbox_acls.choices import (
7 | ACLActionChoices,
8 | ACLProtocolChoices,
9 | ACLRuleActionChoices,
10 | ACLTypeChoices,
11 | )
12 | from netbox_acls.models import AccessList, ACLExtendedRule, ACLStandardRule
13 |
14 |
15 | class ACLStandardRuleAPIViewTestCase(APIViewTestCases.APIViewTestCase):
16 | """
17 | API view test case for ACLStandardRule.
18 | """
19 |
20 | model = ACLStandardRule
21 | view_namespace = "plugins-api:netbox_acls"
22 | brief_fields = ["access_list", "display", "id", "index", "url"]
23 | user_permissions = (
24 | "dcim.view_site",
25 | "dcim.view_manufacturer",
26 | "dcim.view_devicetype",
27 | "dcim.view_device",
28 | "ipam.view_prefix",
29 | "virtualization.view_cluster",
30 | "virtualization.view_clustergroup",
31 | "virtualization.view_clustertype",
32 | "virtualization.view_virtualmachine",
33 | "netbox_acls.view_accesslist",
34 | )
35 |
36 | @classmethod
37 | def setUpTestData(cls):
38 | """Set up ACL Standard Rule for API view testing."""
39 | site = Site.objects.create(
40 | name="Site 1",
41 | slug="site-1",
42 | )
43 |
44 | # Device
45 | manufacturer = Manufacturer.objects.create(
46 | name="Manufacturer 1",
47 | slug="manufacturer-1",
48 | )
49 | device_type = DeviceType.objects.create(
50 | manufacturer=manufacturer,
51 | model="Device Type 1",
52 | )
53 | device_role = DeviceRole.objects.create(
54 | name="Device Role 1",
55 | slug="device-role-1",
56 | )
57 | device = Device.objects.create(
58 | name="Device 1",
59 | site=site,
60 | device_type=device_type,
61 | role=device_role,
62 | )
63 |
64 | # Virtual Machine
65 | cluster_type = ClusterType.objects.create(
66 | name="Cluster Type 1",
67 | slug="cluster-type-1",
68 | )
69 | cluster = Cluster.objects.create(
70 | name="Cluster 1",
71 | type=cluster_type,
72 | )
73 | virtual_machine = VirtualMachine.objects.create(
74 | name="VM 1",
75 | cluster=cluster,
76 | )
77 |
78 | # AccessList
79 | access_list_device = AccessList.objects.create(
80 | name="testacl1",
81 | assigned_object=device,
82 | type=ACLTypeChoices.TYPE_STANDARD,
83 | default_action=ACLActionChoices.ACTION_DENY,
84 | )
85 | access_list_vm = AccessList.objects.create(
86 | name="testacl2",
87 | assigned_object=virtual_machine,
88 | type=ACLTypeChoices.TYPE_STANDARD,
89 | default_action=ACLActionChoices.ACTION_PERMIT,
90 | )
91 |
92 | # Prefix
93 | prefix1 = Prefix.objects.create(
94 | prefix="10.0.0.0/24",
95 | )
96 | prefix2 = Prefix.objects.create(
97 | prefix="10.0.1.0/24",
98 | )
99 |
100 | acl_standard_rules = (
101 | ACLStandardRule(
102 | access_list=access_list_device,
103 | index=10,
104 | description="Rule 10",
105 | action=ACLRuleActionChoices.ACTION_PERMIT,
106 | source_prefix=prefix1,
107 | ),
108 | ACLStandardRule(
109 | access_list=access_list_device,
110 | index=20,
111 | description="Rule 20",
112 | action=ACLRuleActionChoices.ACTION_REMARK,
113 | remark="Remark 1",
114 | ),
115 | ACLStandardRule(
116 | access_list=access_list_vm,
117 | index=10,
118 | description="Rule 10",
119 | action=ACLRuleActionChoices.ACTION_DENY,
120 | source_prefix=prefix2,
121 | ),
122 | )
123 | ACLStandardRule.objects.bulk_create(acl_standard_rules)
124 |
125 | cls.create_data = [
126 | {
127 | "access_list": access_list_device.id,
128 | "index": 30,
129 | "description": "Rule 30",
130 | "action": ACLRuleActionChoices.ACTION_DENY,
131 | "source_prefix": prefix2.id,
132 | },
133 | {
134 | "access_list": access_list_vm.id,
135 | "index": 20,
136 | "description": "Rule 30",
137 | "action": ACLRuleActionChoices.ACTION_PERMIT,
138 | "source_prefix": prefix1.id,
139 | },
140 | {
141 | "access_list": access_list_vm.id,
142 | "index": 30,
143 | "description": "Rule 30",
144 | "action": ACLRuleActionChoices.ACTION_REMARK,
145 | "remark": "Remark 2",
146 | },
147 | ]
148 | cls.bulk_update_data = {
149 | "description": "Rule bulk update",
150 | }
151 |
152 |
153 | class ACLExtendedRuleAPIViewTestCase(APIViewTestCases.APIViewTestCase):
154 | """
155 | API view test case for ACLExtendedRule.
156 | """
157 |
158 | model = ACLExtendedRule
159 | view_namespace = "plugins-api:netbox_acls"
160 | brief_fields = ["access_list", "display", "id", "index", "url"]
161 | user_permissions = (
162 | "dcim.view_site",
163 | "dcim.view_manufacturer",
164 | "dcim.view_devicetype",
165 | "dcim.view_device",
166 | "ipam.view_prefix",
167 | "virtualization.view_cluster",
168 | "virtualization.view_clustergroup",
169 | "virtualization.view_clustertype",
170 | "virtualization.view_virtualmachine",
171 | "netbox_acls.view_accesslist",
172 | )
173 |
174 | @classmethod
175 | def setUpTestData(cls):
176 | """Set up ACL Extended Rule for API view testing."""
177 | site = Site.objects.create(
178 | name="Site 1",
179 | slug="site-1",
180 | )
181 |
182 | # Device
183 | manufacturer = Manufacturer.objects.create(
184 | name="Manufacturer 1",
185 | slug="manufacturer-1",
186 | )
187 | device_type = DeviceType.objects.create(
188 | manufacturer=manufacturer,
189 | model="Device Type 1",
190 | )
191 | device_role = DeviceRole.objects.create(
192 | name="Device Role 1",
193 | slug="device-role-1",
194 | )
195 | device = Device.objects.create(
196 | name="Device 1",
197 | site=site,
198 | device_type=device_type,
199 | role=device_role,
200 | )
201 |
202 | # Virtual Machine
203 | cluster_type = ClusterType.objects.create(
204 | name="Cluster Type 1",
205 | slug="cluster-type-1",
206 | )
207 | cluster = Cluster.objects.create(
208 | name="Cluster 1",
209 | type=cluster_type,
210 | )
211 | virtual_machine = VirtualMachine.objects.create(
212 | name="VM 1",
213 | cluster=cluster,
214 | )
215 |
216 | # AccessList
217 | access_list_device = AccessList.objects.create(
218 | name="testacl1",
219 | assigned_object=device,
220 | type=ACLTypeChoices.TYPE_EXTENDED,
221 | default_action=ACLActionChoices.ACTION_DENY,
222 | )
223 | access_list_vm = AccessList.objects.create(
224 | name="testacl2",
225 | assigned_object=virtual_machine,
226 | type=ACLTypeChoices.TYPE_EXTENDED,
227 | default_action=ACLActionChoices.ACTION_PERMIT,
228 | )
229 |
230 | # Prefix
231 | prefix1 = Prefix.objects.create(
232 | prefix="10.0.0.0/24",
233 | )
234 | prefix2 = Prefix.objects.create(
235 | prefix="10.0.1.0/24",
236 | )
237 |
238 | acl_extended_rules = (
239 | ACLExtendedRule(
240 | access_list=access_list_device,
241 | index=10,
242 | description="Rule 10",
243 | action=ACLRuleActionChoices.ACTION_PERMIT,
244 | protocol=ACLProtocolChoices.PROTOCOL_TCP,
245 | source_prefix=prefix1,
246 | source_ports=[22, 443],
247 | destination_prefix=prefix1,
248 | destination_ports=[22, 443],
249 | ),
250 | ACLExtendedRule(
251 | access_list=access_list_device,
252 | index=20,
253 | description="Rule 20",
254 | action=ACLRuleActionChoices.ACTION_REMARK,
255 | remark="Remark 1",
256 | ),
257 | ACLExtendedRule(
258 | access_list=access_list_vm,
259 | index=10,
260 | description="Rule 10",
261 | action=ACLRuleActionChoices.ACTION_DENY,
262 | source_prefix=prefix2,
263 | destination_prefix=prefix1,
264 | ),
265 | )
266 | ACLExtendedRule.objects.bulk_create(acl_extended_rules)
267 |
268 | cls.create_data = [
269 | {
270 | "access_list": access_list_device.id,
271 | "index": 30,
272 | "description": "Rule 30",
273 | "action": ACLRuleActionChoices.ACTION_DENY,
274 | "protocol": ACLProtocolChoices.PROTOCOL_UDP,
275 | "source_prefix": prefix2.id,
276 | "source_ports": [53],
277 | "destination_prefix": prefix2.id,
278 | "destination_ports": [53],
279 | },
280 | {
281 | "access_list": access_list_vm.id,
282 | "index": 20,
283 | "description": "Rule 30",
284 | "action": ACLRuleActionChoices.ACTION_PERMIT,
285 | "protocol": ACLProtocolChoices.PROTOCOL_ICMP,
286 | "source_prefix": prefix1.id,
287 | "destination_prefix": prefix2.id,
288 | },
289 | {
290 | "access_list": access_list_vm.id,
291 | "index": 30,
292 | "description": "Rule 30",
293 | "action": ACLRuleActionChoices.ACTION_REMARK,
294 | "remark": "Remark 2",
295 | },
296 | ]
297 | cls.bulk_update_data = {
298 | "description": "Rule bulk update",
299 | }
300 |
--------------------------------------------------------------------------------