├── 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 |
18 | {% csrf_token %} 19 |
20 |
21 | {% include 'htmx/table.html' %} 22 |
23 |
24 |
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 | 7 | {% endblock breadcrumbs %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |

ACL Interface Assignment

14 | 15 | 16 | 17 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
Host 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 |
Interface{{ object.assigned_object|linkify|placeholder }}
Access List{{ object.access_list|linkify }}
Direction{% badge object.get_direction_display bg_color=object.get_direction_color %}
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 | 6 | {% endblock breadcrumbs %} 7 | 8 | {% block content %} 9 |
10 |
11 |
12 |

ACL Standard Rule

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
Access List{{ object.access_list|linkify }}
Index{{ object.index }}
Description{{ object.description|placeholder }}
27 |
28 | {% include 'inc/panels/custom_fields.html' %} 29 | {% include 'inc/panels/tags.html' %} 30 |
31 |
32 |
33 |

Details

34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
Action{% badge object.get_action_display bg_color=object.get_action_color %}
Remark{{ object.remark|placeholder }}
Source Prefix{{ object.source_prefix|linkify|placeholder }}
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 | 6 | {% endblock breadcrumbs %} 7 | 8 | {% block content %} 9 |
10 |
11 |
12 |

ACL Extended Rule

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
Access List{{ object.access_list|linkify }}
Description{{ object.description|placeholder }}
Index{{ object.index }}
27 |
28 | {% include 'inc/panels/custom_fields.html' %} 29 | {% include 'inc/panels/tags.html' %} 30 |
31 |
32 |
33 |

Details

34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
Action{% badge object.get_action_display bg_color=object.get_action_color %}
Remark{{ object.remark|placeholder }}
Protocol{{ object.get_protocol_display|placeholder }}
Source Prefix{{ object.source_prefix|linkify|placeholder }}
Source Ports{{ object.source_ports|join:", "|placeholder }}
Destination Prefix{{ object.destination_prefix|linkify|placeholder }}
Destination Ports{{ object.destination_ports|join:", "|placeholder }}
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 |

Access List

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% if object.type == 'standard' %} 36 | 39 | {% elif object.type == 'extended' %} 40 | 43 | {% endif %} 44 | 45 | 46 | 47 | 48 | 49 |
Type{% badge object.get_type_display bg_color=object.get_type_color %}
Default Action{% badge object.get_default_action_display bg_color=object.get_default_action_color %}
Rules 37 | {{ object.aclstandardrules.count|placeholder }} 38 | 41 | {{ object.aclextendedrules.count|placeholder }} 42 |
Assigned Host{{ object.assigned_object|linkify }}
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 |

{{ object.get_type_display }} Rules

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 | ![Access List - List View](docs/img/access_lists.png) 108 | 109 | Access List (Type Extended) - Individual View 110 | ![Access List Type Extended - Individual View](docs/img/access_list_type_extended.png) 111 | 112 | Access List (Type Standard) - Individual View 113 | ![Access List Type Standard - Individual View](docs/img/access_list_type_standard.png) 114 | 115 | Extended Access List Rules - List View 116 | ![Extended Access List Rules - List View](docs/img/acl_extended_rules.png) 117 | 118 | Standard Access List Rules - List View 119 | ![Standard Access List Rules - List View](docs/img/acl_standard_rules.png) 120 | 121 | Access List Interface Assignments- List View 122 | ![Access List Interface Assignments- List View](docs/img/acl_interface_assignments.png) 123 | 124 | Host (device, virtual_chassis, virtual_machine) Access Lists - New Card 125 | ![Host Access Lists - New Card](docs/img/acl_host_view.png) 126 | 127 | Host Interface (vminterface interface) Access Lists - New Card 128 | ![Host Interface Access Lists - New Card](docs/img/access_list_type_standard.png) 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 | --------------------------------------------------------------------------------