diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5cf0c151..08e4333b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Default owner(s) of all files in this repository -* @whitej6 @itdependsnetworks @jathanism @jdrew82 +* @whitej6 @itdependsnetworks @jdrew82 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 150b1f8f..5fd8de71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: fail-fast: true matrix: python-version: ["3.7"] - nautobot-version: ["1.3.4"] + nautobot-version: ["1.4.1"] env: INVOKE_NAUTOBOT_FIREWALL_MODELS_PYTHON_VER: "${{ matrix.python-version }}" INVOKE_NAUTOBOT_FIREWALL_MODELS_NAUTOBOT_VER: "${{ matrix.nautobot-version }}" @@ -120,14 +120,14 @@ jobs: include: - python-version: "3.10" db-backend: "postgresql" - nautobot-version: "1.3.3" + nautobot-version: "1.4.1" # TODO: Include the following, once mysql is working on CI # - python-version: "3.7" # db-backend: "mysql" - # nautobot-version: "1.3.3" + # nautobot-version: "1.4.1" # - python-version: "3.10" # db-backend: "mysql" - # nautobot-version: "stable" + # nautobot-version: "latest" runs-on: "ubuntu-20.04" env: INVOKE_NAUTOBOT_FIREWALL_MODELS_PYTHON_VER: "${{ matrix.python-version }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 38e37a96..6c5f83f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,52 @@ # Changelog +## v1.0.0 - 2022-08-27 + +### Removed + +- #80 Support for Nautobot < v1.4.0 + +### Changed + +- #80 All plural attrs on PolicyRule are now represented in plural form (`source_user` is now `source_users` etc). +- #80 Nav menu name from `Firewall` to `Security`. +- #80 Styling on PolicyRule detail tables + +### Added + +- #80 Support for Notes +- #80 Source Service suport +- #80 Security panel on homepage +- #80 PolicyRule detail tables convert empty value to `ANY` + ## v0.1.0-beta.3 - 2022-07-19 ### Changed + - #68 Update Policy Rules Expanded to be more intuitive - #69 Change to use arrow in UI elements ### Added + - #63 Capirca Integration + ## v0.1.0-beta.2 - 2022-07-10 ### Changed + - Update Serializers to current standards - Update development environment to current standards - Update CI matrix for better runtime & coverage of versions ### Fixed + - Pydocstyle.ini being properly used - Dockerfile to work with `NAUTOBOT_VER` from build args, previously poetry superceeded the build arg. - Updates for `status` attributes to account for defaulting. - Static URLs for images in `README.md` to fix broken link in PyPI page rendering. ### Added + - Ability to expand Policy detail API view with query param `deep=True`. - Added `to_json` method on `Policy` and `PolicyRule` models. - Added `rule_details` as a helper for working with a `PolicyRule` @@ -31,9 +56,11 @@ ## v0.1.0-beta.1 - 2022-07-08 ### Fixed + - Issues with docs rendering in PyPI & ReadTheDocs ## v0.1.0-beta.2 - 2022-07-08 ### Announcements -- Initial Release \ No newline at end of file + +- Initial Release diff --git a/README.md b/README.md index 2b43de00..f58b59f8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The plugin is available as a Python package in PyPI and can be installed with `p pip install nautobot-firewall-models ``` -> The plugin is compatible with Nautobot 1.3.0 and higher +> The plugin is compatible with Nautobot 1.4.0 and higher To ensure Nautobot Firewall Models Plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-firewall-models` package: diff --git a/development/Dockerfile b/development/Dockerfile index 2b513529..64682cb8 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -1,4 +1,4 @@ -ARG NAUTOBOT_VER="1.3.1" +ARG NAUTOBOT_VER="1.4.1" ARG PYTHON_VER=3.8 FROM ghcr.io/nautobot/nautobot-dev:${NAUTOBOT_VER}-py${PYTHON_VER} @@ -27,7 +27,7 @@ COPY poetry.lock pyproject.toml /tmp/install/ # Otherwise Poetry will override the version in this container # with the one in the poetry.lock # Redifine NAUTOBOT_VER as a build arg as initial is defined outside FROM. -ARG NAUTOBOT_VER="1.3.1" +ARG NAUTOBOT_VER="1.4.1" RUN poetry add nautobot@$NAUTOBOT_VER # --no-root declares not to install the project package since we're wanting to take advantage of caching dependency installation diff --git a/docs/images/datamodel.png b/docs/images/datamodel.png index d9bfbc09..f5c17965 100644 Binary files a/docs/images/datamodel.png and b/docs/images/datamodel.png differ diff --git a/docs/images/navmenu.png b/docs/images/navmenu.png index 77f3ffeb..153ebd9b 100644 Binary files a/docs/images/navmenu.png and b/docs/images/navmenu.png differ diff --git a/docs/images/policy.png b/docs/images/policy.png index 9ba5e583..6d8d3e71 100644 Binary files a/docs/images/policy.png and b/docs/images/policy.png differ diff --git a/docs/index.md b/docs/index.md index 77e6f9dd..85e2cca6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ The plugin is available as a Python package in PyPI and can be installed with `p pip install nautobot-firewall-models ``` -> The plugin is compatible with Nautobot 1.3.0 and higher +> The plugin is compatible with Nautobot 1.4.0 and higher To ensure Nautobot Firewall Models Plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the Nautobot root directory (alongside `requirements.txt`) and list the `nautobot-firewall-models` package: diff --git a/docs/models.md b/docs/models.md index 5b714332..172684d0 100644 --- a/docs/models.md +++ b/docs/models.md @@ -51,11 +51,6 @@ This plugin uses [custom models to model many-to-many](https://docs.djangoprojec heading-offset=2 %} -{% - include-markdown "./models/policyrulem2m.md" - heading-offset=2 -%} - {% include-markdown "./models/policydevicem2m.md" heading-offset=2 diff --git a/docs/models/policyrule.md b/docs/models/policyrule.md index fc760bc6..3a856315 100644 --- a/docs/models/policyrule.md +++ b/docs/models/policyrule.md @@ -23,6 +23,10 @@ It is recommended to use a descriptive name to best identify a PolicyRule, other * Log (boolean) * Request ID (optional, string) * Meant to represent an upstream request (e.g. an service request from an ITSM solution). +* Index (optional, int) + * Sets the index of the PolicyRule in the Policy. + * Example `20 permit icmp host 1.1.1.1 any` would have an index of `20`. + * Set as optional for now, will be set to required at a later date with default as the highest value + 10. ## Examples diff --git a/docs/models/policyrulem2m.md b/docs/models/policyrulem2m.md deleted file mode 100644 index dd231882..00000000 --- a/docs/models/policyrulem2m.md +++ /dev/null @@ -1,16 +0,0 @@ -# PolicyRuleM2M - -Allows for creating an index value that is only relevant to the relationship, this allows for a Policy Rule to potentially be used multiple times across multiple Policies. - -This model is not directly exposed to the user but can be accessed via the Policy object, and the index value is set in the Policy detail view. - -## Attributes - -* Index (optional, int) - * Sets the index of the PolicyRule in the Policy. - * Example `20 permit icmp host 1.1.1.1 any` would have an index of `20`. - * Must be unique. - * Set as optional for now, will be set to required at a later date with default as the highest value + 10. - * Uniqueness does not apply when not set. -* Policy (FK to Policy) -* Policy Rules (FK to PolicyRule) diff --git a/nautobot_firewall_models/__init__.py b/nautobot_firewall_models/__init__.py index 9d37c98e..a6cf3b07 100644 --- a/nautobot_firewall_models/__init__.py +++ b/nautobot_firewall_models/__init__.py @@ -21,7 +21,7 @@ class NautobotFirewallModelsConfig(PluginConfig): description = "Nautobot plugin to model firewall objects.." base_url = "firewall" required_settings = [] - min_version = "1.3.0" + min_version = "1.4.0" max_version = "1.9999" default_settings = {"capirca_remark_pass": True, "capirca_os_map": {}, "allowed_status": ["active"]} caching_config = {"*": {"timeout": 0}} diff --git a/nautobot_firewall_models/api/serializers.py b/nautobot_firewall_models/api/serializers.py index 5126db4a..e03efd5d 100644 --- a/nautobot_firewall_models/api/serializers.py +++ b/nautobot_firewall_models/api/serializers.py @@ -10,6 +10,7 @@ from nautobot.extras.api.serializers import ( StatusModelSerializerMixin as _StatusModelSerializerMixin, TaggedObjectSerializer, + NautobotModelSerializer, ) from nautobot.extras.models import DynamicGroup, Status from nautobot.ipam.models import IPAddress @@ -30,9 +31,7 @@ class StatusModelSerializerMixin(_StatusModelSerializerMixin): # pylint: disabl status = StatusSerializerField(queryset=Status.objects.all(), required=False) -class IPRangeSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class IPRangeSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """IPRange Serializer.""" url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_firewall_models-api:iprange-detail") @@ -46,9 +45,7 @@ class Meta: fields = "__all__" -class FQDNSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class FQDNSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """FQDN Serializer.""" url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_firewall_models-api:fqdn-detail") @@ -63,9 +60,7 @@ class Meta: fields = "__all__" -class AddressObjectSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class AddressObjectSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """AddressObject Serializer.""" url = serializers.HyperlinkedIdentityField( @@ -83,9 +78,7 @@ class Meta: fields = "__all__" -class AddressObjectGroupSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class AddressObjectGroupSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """AddressObjectGroup Serializer.""" url = serializers.HyperlinkedIdentityField( @@ -102,9 +95,7 @@ class Meta: fields = "__all__" -class ServiceObjectSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class ServiceObjectSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """ServiceObject Serializer.""" url = serializers.HyperlinkedIdentityField( @@ -118,9 +109,7 @@ class Meta: fields = "__all__" -class ServiceObjectGroupSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class ServiceObjectGroupSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """ServiceObjectGroup Serializer.""" url = serializers.HyperlinkedIdentityField( @@ -139,9 +128,7 @@ class Meta: fields = "__all__" -class UserObjectSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class UserObjectSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """UserObject Serializer.""" url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_firewall_models-api:userobject-detail") @@ -153,9 +140,7 @@ class Meta: fields = "__all__" -class UserObjectGroupSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class UserObjectGroupSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """UserObjectGroup Serializer.""" url = serializers.HyperlinkedIdentityField( @@ -172,9 +157,7 @@ class Meta: fields = "__all__" -class ZoneSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class ZoneSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """Zone Serializer.""" url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_firewall_models-api:zone-detail") @@ -187,42 +170,40 @@ class Meta: fields = "__all__" -class PolicyRuleSerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class PolicyRuleSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """PolicyRule Serializer.""" url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_firewall_models-api:policyrule-detail") - source_user = SerializedPKRelatedField( + source_users = SerializedPKRelatedField( queryset=models.UserObject.objects.all(), serializer=UserObjectSerializer, required=False, many=True ) - source_user_group = SerializedPKRelatedField( + source_user_groups = SerializedPKRelatedField( queryset=models.UserObjectGroup.objects.all(), serializer=UserObjectGroupSerializer, required=False, many=True ) - source_address = SerializedPKRelatedField( + source_addresses = SerializedPKRelatedField( queryset=models.AddressObject.objects.all(), serializer=AddressObjectSerializer, required=False, many=True ) - source_address_group = SerializedPKRelatedField( + source_address_groups = SerializedPKRelatedField( queryset=models.AddressObjectGroup.objects.all(), serializer=AddressObjectGroupSerializer, required=False, many=True, ) source_zone = ZoneSerializer(required=False) - destination_address = SerializedPKRelatedField( + destination_addresses = SerializedPKRelatedField( queryset=models.AddressObject.objects.all(), serializer=AddressObjectSerializer, required=False, many=True ) - destination_address_group = SerializedPKRelatedField( + destination_address_groups = SerializedPKRelatedField( queryset=models.AddressObjectGroup.objects.all(), serializer=AddressObjectGroupSerializer, required=False, many=True, ) destination_zone = ZoneSerializer(required=False) - service = SerializedPKRelatedField( + destination_services = SerializedPKRelatedField( queryset=models.ServiceObject.objects.all(), serializer=ServiceObjectSerializer, required=False, many=True ) - service_group = SerializedPKRelatedField( + destination_service_groups = SerializedPKRelatedField( queryset=models.ServiceObjectGroup.objects.all(), serializer=ServiceObjectGroupSerializer, required=False, @@ -243,7 +224,7 @@ class Meta: """Meta attributes.""" model = models.PolicyRuleM2M - fields = ["rule", "index"] + fields = ["rule"] class PolicyRuleM2MDeepNestedSerializer(PolicyRuleM2MNestedSerializer): @@ -272,13 +253,10 @@ class Meta: fields = ["dynamic_group", "weight"] -class PolicySerializer( - TaggedObjectSerializer, StatusModelSerializerMixin, CustomFieldModelSerializer, ValidatedModelSerializer -): +class PolicySerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """Policy Serializer.""" url = serializers.HyperlinkedIdentityField(view_name="plugins-api:nautobot_firewall_models-api:policy-detail") - policy_rules = PolicyRuleM2MNestedSerializer(many=True, required=False, source="policyrulem2m_set") assigned_devices = PolicyDeviceM2MNestedSerializer(many=True, required=False, source="policydevicem2m_set") assigned_dynamic_groups = PolicyDynamicGroupM2MNestedSerializer( many=True, required=False, source="policydynamicgroupm2m_set" @@ -292,13 +270,10 @@ class Meta: def create(self, validated_data): """Overload create to account for custom m2m field.""" - policy_rules = validated_data.pop("policyrulem2m_set", None) assigned_devices = validated_data.pop("policydevicem2m_set", None) assigned_dynamic_groups = validated_data.pop("policydynamicgroupm2m_set", None) instance = super().create(validated_data) - if policy_rules is not None: - return self._save_policy_rules(instance, policy_rules) if assigned_devices is not None: return self._save_assigned_devices(instance, assigned_devices) if assigned_dynamic_groups is not None: @@ -308,14 +283,11 @@ def create(self, validated_data): def update(self, instance, validated_data): """Overload create to account for update m2m field.""" - policy_rules = validated_data.pop("policyrulem2m_set", None) assigned_devices = validated_data.pop("policydevicem2m_set", None) assigned_dynamic_groups = validated_data.pop("policydynamicgroupm2m_set", None) instance = super().update(instance, validated_data) - if policy_rules is not None: - return self._save_policy_rules(instance, policy_rules) if assigned_devices is not None: return self._save_assigned_devices(instance, assigned_devices) if assigned_dynamic_groups is not None: @@ -323,19 +295,6 @@ def update(self, instance, validated_data): return instance - def _save_policy_rules(self, instance, policy_rules): - # pylint: disable=R0201 - """Helper function for custom m2m field.""" - instance.policy_rules.clear() - for p_r in policy_rules: - models.PolicyRuleM2M.objects.create( - rule=models.PolicyRule.objects.get(id=p_r["rule"].id), - index=p_r.get("index", None), - policy=instance, - ) - - return instance - def _save_assigned_devices(self, instance, assigned_devices): # pylint: disable=R0201 """Helper function for custom m2m field.""" @@ -357,7 +316,7 @@ def _save_assigned_dynamic_groups(self, instance, assigned_dynamic_groups): for d_g in assigned_dynamic_groups: models.PolicyDynamicGroupM2M.objects.create( dynamic_group=DynamicGroup.objects.get(id=d_g["dynamic_group"].id), - index=d_g.get("weight", None), + weight=d_g.get("weight", None), policy=instance, ) @@ -368,7 +327,6 @@ def validate(self, data): """Overload validate to pop field for custom m2m relationship.""" # Remove custom fields data and tags (if any) prior to model validation attrs = data.copy() - attrs.pop("policyrulem2m_set", None) attrs.pop("policydevicem2m_set", None) attrs.pop("policydynamicgroupm2m_set", None) super().validate(attrs) @@ -378,7 +336,9 @@ def validate(self, data): class PolicyDeepSerializer(PolicySerializer): """Overload for create & update views.""" - policy_rules = PolicyRuleM2MDeepNestedSerializer(many=True, required=False, source="policyrulem2m_set") + policy_rules = SerializedPKRelatedField( + queryset=models.PolicyRule.objects.all(), serializer=PolicyRuleSerializer, required=False, many=True + ) class CapircaPolicySerializer(TaggedObjectSerializer, CustomFieldModelSerializer, ValidatedModelSerializer): diff --git a/nautobot_firewall_models/api/views.py b/nautobot_firewall_models/api/views.py index 76bed2c9..aab15598 100644 --- a/nautobot_firewall_models/api/views.py +++ b/nautobot_firewall_models/api/views.py @@ -2,12 +2,13 @@ from nautobot.core.api.views import ModelViewSet from nautobot.core.settings_funcs import is_truthy +from nautobot.extras.api.views import NautobotModelViewSet from nautobot_firewall_models import filters, models from nautobot_firewall_models.api import serializers -class IPRangeViewSet(ModelViewSet): +class IPRangeViewSet(NautobotModelViewSet): """IPRange viewset.""" queryset = models.IPRange.objects.all() @@ -15,7 +16,7 @@ class IPRangeViewSet(ModelViewSet): filterset_class = filters.IPRangeFilterSet -class FQDNViewSet(ModelViewSet): +class FQDNViewSet(NautobotModelViewSet): """FQDN viewset.""" queryset = models.FQDN.objects.all() @@ -23,7 +24,7 @@ class FQDNViewSet(ModelViewSet): filterset_class = filters.FQDNFilterSet -class AddressObjectViewSet(ModelViewSet): +class AddressObjectViewSet(NautobotModelViewSet): """AddressObject viewset.""" queryset = models.AddressObject.objects.all() @@ -31,7 +32,7 @@ class AddressObjectViewSet(ModelViewSet): filterset_class = filters.AddressObjectFilterSet -class AddressObjectGroupViewSet(ModelViewSet): +class AddressObjectGroupViewSet(NautobotModelViewSet): """AddressObjectGroup viewset.""" queryset = models.AddressObjectGroup.objects.all() @@ -39,7 +40,7 @@ class AddressObjectGroupViewSet(ModelViewSet): filterset_class = filters.AddressObjectGroupFilterSet -class ServiceObjectViewSet(ModelViewSet): +class ServiceObjectViewSet(NautobotModelViewSet): """ServiceObject viewset.""" queryset = models.ServiceObject.objects.all() @@ -47,7 +48,7 @@ class ServiceObjectViewSet(ModelViewSet): filterset_class = filters.ServiceObjectFilterSet -class ServiceObjectGroupViewSet(ModelViewSet): +class ServiceObjectGroupViewSet(NautobotModelViewSet): """ServiceObjectGroup viewset.""" queryset = models.ServiceObjectGroup.objects.all() @@ -55,7 +56,7 @@ class ServiceObjectGroupViewSet(ModelViewSet): filterset_class = filters.ServiceObjectGroupFilterSet -class UserObjectViewSet(ModelViewSet): +class UserObjectViewSet(NautobotModelViewSet): """UserObject viewset.""" queryset = models.UserObject.objects.all() @@ -63,7 +64,7 @@ class UserObjectViewSet(ModelViewSet): filterset_class = filters.UserObjectFilterSet -class UserObjectGroupViewSet(ModelViewSet): +class UserObjectGroupViewSet(NautobotModelViewSet): """UserObjectGroup viewset.""" queryset = models.UserObjectGroup.objects.all() @@ -71,7 +72,7 @@ class UserObjectGroupViewSet(ModelViewSet): filterset_class = filters.UserObjectGroupFilterSet -class ZoneViewSet(ModelViewSet): +class ZoneViewSet(NautobotModelViewSet): """Zone viewset.""" queryset = models.Zone.objects.all() @@ -79,7 +80,7 @@ class ZoneViewSet(ModelViewSet): filterset_class = filters.ZoneFilterSet -class PolicyRuleViewSet(ModelViewSet): +class PolicyRuleViewSet(NautobotModelViewSet): """PolicyRule viewset.""" queryset = models.PolicyRule.objects.all() @@ -87,7 +88,7 @@ class PolicyRuleViewSet(ModelViewSet): filterset_class = filters.PolicyRuleFilterSet -class PolicyViewSet(ModelViewSet): +class PolicyViewSet(NautobotModelViewSet): """Policy viewset.""" queryset = models.Policy.objects.all() diff --git a/nautobot_firewall_models/forms.py b/nautobot_firewall_models/forms.py index f110a4ba..c2323442 100644 --- a/nautobot_firewall_models/forms.py +++ b/nautobot_firewall_models/forms.py @@ -3,14 +3,14 @@ from django import forms from nautobot.dcim.models import Interface, Device from nautobot.extras.forms import ( - AddRemoveTagsForm, - StatusFilterFormMixin, - StatusBulkEditFormMixin, - CustomFieldFilterForm, - CustomFieldBulkEditForm, + TagsBulkEditFormMixin, + StatusModelFilterFormMixin, + StatusModelBulkEditFormMixin, + CustomFieldModelFilterFormMixin, + CustomFieldModelBulkEditFormMixin, CustomFieldModelCSVForm, - CustomFieldModelForm, - RelationshipModelForm, + CustomFieldModelFormMixin, + RelationshipModelFormMixin, ) from nautobot.extras.models import Tag, DynamicGroup from nautobot.ipam.models import VRF, Prefix, IPAddress @@ -28,7 +28,7 @@ from nautobot_firewall_models import models, fields, choices -class IPRangeFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class IPRangeFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "start_address", "end_address", "vrf"] @@ -44,7 +44,7 @@ class IPRangeFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilter vrf = DynamicModelChoiceField(queryset=VRF.objects.all(), label="VRF", required=False) -class IPRangeForm(BootstrapMixin, fields.IPRangeFieldMixin, forms.ModelForm): +class IPRangeForm(BootstrapMixin, fields.IPRangeFieldMixin, RelationshipModelFormMixin, forms.ModelForm): """IPRange creation/edit form.""" vrf = DynamicModelChoiceField(queryset=VRF.objects.all(), label="VRF", required=False) @@ -56,7 +56,7 @@ class Meta: fields = ["vrf", "description", "status", "tags"] -class IPRangeBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class IPRangeBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """IPRange bulk edit form.""" pk = DynamicModelMultipleChoiceField(queryset=models.IPRange.objects.all(), widget=forms.MultipleHiddenInput) @@ -71,7 +71,7 @@ class Meta: nullable_fields = ["description", "vrf"] -class FQDNFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class FQDNFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "name"] @@ -85,7 +85,7 @@ class FQDNFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterFor name = forms.CharField(required=False, label="Name") -class FQDNForm(BootstrapMixin, forms.ModelForm): +class FQDNForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): """FQDN creation/edit form.""" ip_addresses = DynamicModelMultipleChoiceField(queryset=IPAddress.objects.all(), required=False) @@ -97,7 +97,7 @@ class Meta: fields = ["name", "description", "ip_addresses", "status", "tags"] -class FQDNBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class FQDNBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """FQDN bulk edit form.""" pk = DynamicModelMultipleChoiceField(queryset=models.FQDN.objects.all(), widget=forms.MultipleHiddenInput) @@ -110,7 +110,7 @@ class Meta: nullable_fields = ["description", "ip_addresses"] -class AddressObjectFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class AddressObjectFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "name"] @@ -128,7 +128,7 @@ class AddressObjectFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomField fqdn = DynamicModelChoiceField(queryset=models.FQDN.objects.all(), required=False, label="FQDN") -class AddressObjectForm(BootstrapMixin, forms.ModelForm): +class AddressObjectForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): """AddressObject creation/edit form.""" ip_address = DynamicModelChoiceField(queryset=IPAddress.objects.all(), required=False, label="IP Address") @@ -143,7 +143,7 @@ class Meta: fields = ["name", "description", "fqdn", "ip_range", "ip_address", "prefix", "status", "tags"] -class AddressObjectBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class AddressObjectBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """AddressObject bulk edit form.""" pk = DynamicModelMultipleChoiceField(queryset=models.AddressObject.objects.all(), widget=forms.MultipleHiddenInput) @@ -155,7 +155,7 @@ class Meta: nullable_fields = ["description", "fqdn", "ip_range", "ip_address", "prefix"] -class AddressObjectGroupFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class AddressObjectGroupFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "name"] @@ -169,7 +169,7 @@ class AddressObjectGroupFilterForm(BootstrapMixin, StatusFilterFormMixin, Custom name = forms.CharField(required=False, label="Name") -class AddressObjectGroupForm(BootstrapMixin, forms.ModelForm): +class AddressObjectGroupForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): """AddressObjectGroup creation/edit form.""" address_objects = DynamicModelMultipleChoiceField(queryset=models.AddressObject.objects.all()) @@ -181,7 +181,7 @@ class Meta: fields = ["name", "description", "address_objects", "status", "tags"] -class AddressObjectGroupBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class AddressObjectGroupBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """AddressObjectGroup bulk edit form.""" pk = DynamicModelMultipleChoiceField( @@ -197,7 +197,7 @@ class Meta: ] -class ServiceObjectFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class ServiceObjectFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "name"] @@ -213,7 +213,7 @@ class ServiceObjectFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomField ip_protocol = forms.ChoiceField(choices=add_blank_choice(choices.IP_PROTOCOL_CHOICES), required=False) -class ServiceObjectForm(BootstrapMixin, forms.ModelForm): +class ServiceObjectForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): """ServiceObject creation/edit form.""" port = forms.CharField( @@ -228,7 +228,7 @@ class Meta: fields = ["name", "description", "port", "ip_protocol", "status", "tags"] -class ServiceObjectBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class ServiceObjectBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """ServiceObject bulk edit form.""" pk = DynamicModelMultipleChoiceField(queryset=models.ServiceObject.objects.all(), widget=forms.MultipleHiddenInput) @@ -244,7 +244,7 @@ class Meta: nullable_fields = ["description", "port"] -class ServiceObjectGroupFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class ServiceObjectGroupFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "name"] @@ -258,7 +258,7 @@ class ServiceObjectGroupFilterForm(BootstrapMixin, StatusFilterFormMixin, Custom name = forms.CharField(required=False, label="Name") -class ServiceObjectGroupForm(BootstrapMixin, forms.ModelForm): +class ServiceObjectGroupForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): """ServiceObjectGroup creation/edit form.""" service_objects = DynamicModelMultipleChoiceField(queryset=models.ServiceObject.objects.all(), required=False) @@ -270,7 +270,7 @@ class Meta: fields = ["name", "description", "service_objects", "status", "tags"] -class ServiceObjectGroupBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class ServiceObjectGroupBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """ServiceObjectGroup bulk edit form.""" pk = DynamicModelMultipleChoiceField( @@ -286,7 +286,7 @@ class Meta: ] -class UserObjectFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class UserObjectFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "username", "name"] @@ -301,7 +301,7 @@ class UserObjectFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFil username = forms.CharField(required=False, label="Username") -class UserObjectForm(BootstrapMixin, forms.ModelForm): +class UserObjectForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): """UserObject creation/edit form.""" username = forms.CharField(label="Username") @@ -317,7 +317,7 @@ class Meta: fields = ["username", "name", "status", "tags"] -class UserObjectBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class UserObjectBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """UserObject bulk edit form.""" pk = DynamicModelMultipleChoiceField(queryset=models.UserObject.objects.all(), widget=forms.MultipleHiddenInput) @@ -331,7 +331,7 @@ class Meta: ] -class UserObjectGroupFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class UserObjectGroupFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "name"] @@ -345,7 +345,7 @@ class UserObjectGroupFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFie name = forms.CharField(required=False, label="Name") -class UserObjectGroupForm(BootstrapMixin, forms.ModelForm): +class UserObjectGroupForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): """UserObjectGroup creation/edit form.""" user_objects = DynamicModelMultipleChoiceField(queryset=models.UserObject.objects.all(), required=False) @@ -357,7 +357,7 @@ class Meta: fields = ["name", "description", "user_objects", "status", "tags"] -class UserObjectGroupBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class UserObjectGroupBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """UserObjectGroup bulk edit form.""" pk = DynamicModelMultipleChoiceField( @@ -373,7 +373,7 @@ class Meta: ] -class ZoneFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class ZoneFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "name"] @@ -389,7 +389,7 @@ class ZoneFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterFor interfaces = DynamicModelChoiceField(queryset=Interface.objects.all(), label="Interface") -class ZoneForm(BootstrapMixin, forms.ModelForm): +class ZoneForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): """Zone creation/edit form.""" vrfs = DynamicModelMultipleChoiceField(queryset=VRF.objects.all(), required=False, label="VRF") @@ -405,7 +405,7 @@ class Meta: fields = ["name", "description", "vrfs", "device", "interfaces", "status", "tags"] -class ZoneBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class ZoneBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """Zone bulk edit form.""" pk = DynamicModelMultipleChoiceField(queryset=models.Zone.objects.all(), widget=forms.MultipleHiddenInput) @@ -419,7 +419,7 @@ class Meta: nullable_fields = ["description", "vrfs", "interfaces"] -class PolicyRuleFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm): +class PolicyRuleFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" field_order = ["q", "name"] @@ -434,37 +434,43 @@ class PolicyRuleFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFil tag = TagFilterField(models.PolicyRule) -class PolicyRuleForm(BootstrapMixin, CustomFieldModelForm): +class PolicyRuleForm(BootstrapMixin, CustomFieldModelFormMixin, RelationshipModelFormMixin): """PolicyRule creation/edit form.""" name = forms.CharField(required=False, label="Name") tags = DynamicModelMultipleChoiceField(queryset=Tag.objects.all(), required=False) - source_user = DynamicModelMultipleChoiceField( + source_users = DynamicModelMultipleChoiceField( queryset=models.UserObject.objects.all(), label="Source User Objects", required=False ) - source_user_group = DynamicModelMultipleChoiceField( + source_user_groups = DynamicModelMultipleChoiceField( queryset=models.UserObjectGroup.objects.all(), label="Source User Object Groups", required=False ) - source_address = DynamicModelMultipleChoiceField( + source_addresses = DynamicModelMultipleChoiceField( queryset=models.AddressObject.objects.all(), label="Source Address Objects", required=False ) - source_address_group = DynamicModelMultipleChoiceField( + source_address_groups = DynamicModelMultipleChoiceField( queryset=models.AddressObjectGroup.objects.all(), label="Source Address Object Groups", required=False ) source_zone = DynamicModelChoiceField(queryset=models.Zone.objects.all(), label="Source Zone", required=False) - destination_address = DynamicModelMultipleChoiceField( + source_services = DynamicModelMultipleChoiceField( + queryset=models.ServiceObject.objects.all(), label="Service Objects", required=False + ) + source_service_groups = DynamicModelMultipleChoiceField( + queryset=models.ServiceObjectGroup.objects.all(), label="Service Object Groups", required=False + ) + destination_addresses = DynamicModelMultipleChoiceField( queryset=models.AddressObject.objects.all(), label="Destination Address Objects", required=False ) - destination_address_group = DynamicModelMultipleChoiceField( + destination_address_groups = DynamicModelMultipleChoiceField( queryset=models.AddressObjectGroup.objects.all(), label="Destination Address Object Groups", required=False ) destination_zone = DynamicModelChoiceField( queryset=models.Zone.objects.all(), label="Destination Zone", required=False ) - service = DynamicModelMultipleChoiceField( + destination_services = DynamicModelMultipleChoiceField( queryset=models.ServiceObject.objects.all(), label="Service Objects", required=False ) - service_group = DynamicModelMultipleChoiceField( + destination_service_groups = DynamicModelMultipleChoiceField( queryset=models.ServiceObjectGroup.objects.all(), label="Service Object Groups", required=False ) request_id = forms.CharField(required=False, label="Optional field for request ticket identifier.") @@ -476,16 +482,18 @@ class Meta: fields = ( # pylint: disable=duplicate-code "name", - "source_user", - "source_user_group", - "source_address", - "source_address_group", + "source_users", + "source_user_groups", + "source_addresses", + "source_address_groups", "source_zone", - "destination_address", - "destination_address_group", + "source_services", + "source_service_groups", + "destination_addresses", + "destination_address_groups", "destination_zone", - "service", - "service_group", + "destination_services", + "destination_service_groups", "action", "log", "status", @@ -496,12 +504,13 @@ class Meta: # TODO: Refactor -class PolicyRuleBulkEditForm(BootstrapMixin, AddRemoveTagsForm, StatusBulkEditFormMixin, BulkEditForm): +class PolicyRuleBulkEditForm(BootstrapMixin, TagsBulkEditFormMixin, StatusModelBulkEditFormMixin, BulkEditForm): """PolicyRule bulk edit form.""" pk = DynamicModelMultipleChoiceField(queryset=models.PolicyRule.objects.all(), widget=forms.MultipleHiddenInput) action = forms.ChoiceField(choices=add_blank_choice(choices.ACTION_CHOICES), required=False) log = forms.BooleanField(required=False) + description = forms.CharField(required=False) class Meta: """Meta attributes.""" @@ -509,7 +518,7 @@ class Meta: nullable_fields = ["description", "tags"] -class PolicyFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterForm, TenancyFilterForm): +class PolicyFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin, TenancyFilterForm): """Filter form to filter searches.""" field_order = ["q", "name", "assigned_devices", "assigned_dynamic_groups"] @@ -525,7 +534,7 @@ class PolicyFilterForm(BootstrapMixin, StatusFilterFormMixin, CustomFieldFilterF assigned_dynamic_groups = DynamicModelChoiceField(queryset=DynamicGroup.objects.all(), required=False) -class PolicyForm(BootstrapMixin, forms.ModelForm, TenancyForm): +class PolicyForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm, TenancyForm): """Policy creation/edit form.""" assigned_devices = DynamicModelMultipleChoiceField(queryset=Device.objects.all(), required=False) @@ -549,7 +558,7 @@ class Meta: ] -class PolicyBulkEditForm(BootstrapMixin, StatusBulkEditFormMixin, BulkEditForm): +class PolicyBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): """Policy bulk edit form.""" pk = DynamicModelMultipleChoiceField(queryset=models.Policy.objects.all(), widget=forms.MultipleHiddenInput) @@ -570,7 +579,7 @@ class Meta: # CapircaPolicy -class CapircaPolicyForm(BootstrapMixin, CustomFieldModelForm, RelationshipModelForm): +class CapircaPolicyForm(BootstrapMixin, CustomFieldModelFormMixin, RelationshipModelFormMixin): """Filter Form for CapircaPolicy instances.""" device = DynamicModelChoiceField(queryset=Device.objects.all()) @@ -588,7 +597,7 @@ class Meta: ) -class CapircaPolicyFilterForm(BootstrapMixin, CustomFieldFilterForm): +class CapircaPolicyFilterForm(BootstrapMixin, CustomFieldModelFilterFormMixin): """Form for CapircaPolicy instances.""" model = models.CapircaPolicy @@ -596,7 +605,7 @@ class CapircaPolicyFilterForm(BootstrapMixin, CustomFieldFilterForm): q = forms.CharField(required=False, label="Search") -class CapircaPolicyBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): +class CapircaPolicyBulkEditForm(BootstrapMixin, TagsBulkEditFormMixin, CustomFieldModelBulkEditFormMixin): """BulkEdit form for CapircaPolicy instances.""" pk = forms.ModelMultipleChoiceField(queryset=models.CapircaPolicy.objects.all(), widget=forms.MultipleHiddenInput) diff --git a/nautobot_firewall_models/homepage.py b/nautobot_firewall_models/homepage.py new file mode 100644 index 00000000..de085e1a --- /dev/null +++ b/nautobot_firewall_models/homepage.py @@ -0,0 +1,38 @@ +"""Adds plugin items to homepage.""" +from nautobot.core.apps import HomePageItem, HomePagePanel + +from nautobot_firewall_models.models import Policy, PolicyRule, CapircaPolicy + + +layout = ( + HomePagePanel( + weight=150, + name="Security", + items=( + HomePageItem( + name="Policies", + model=Policy, + weight=100, + link="plugins:nautobot_firewall_models:policy_list", + description="Firewall Policies", + permissions=["nautobot_firewall_models.view_policy"], + ), + HomePageItem( + name="Capirca Policies", + model=CapircaPolicy, + weight=150, + link="plugins:nautobot_firewall_models:capircapolicy_list", + description="Firewall Policies", + permissions=["nautobot_firewall_models.view_capircapolicy"], + ), + HomePageItem( + name="Policy Rules", + model=PolicyRule, + weight=200, + link="plugins:nautobot_firewall_models:policyrule_list", + description="Firewall Policies", + permissions=["nautobot_firewall_models.view_policyrule"], + ), + ), + ), +) diff --git a/nautobot_firewall_models/migrations/0006_renaming_part1.py b/nautobot_firewall_models/migrations/0006_renaming_part1.py new file mode 100644 index 00000000..36218a59 --- /dev/null +++ b/nautobot_firewall_models/migrations/0006_renaming_part1.py @@ -0,0 +1,154 @@ +# Generated by Django 3.2.15 on 2022-08-26 18:03 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("nautobot_firewall_models", "0005_capircapolicy"), + ] + + operations = [ + migrations.RenameModel( + old_name="SvcGroupM2M", + new_name="DestSvcGroupM2M", + ), + migrations.RenameModel( + old_name="SvcM2M", + new_name="DestSvcM2M", + ), + migrations.RenameField( + model_name="policyrule", + old_name="destination_address_group", + new_name="destination_address_groups", + ), + migrations.RenameField( + model_name="policyrule", + old_name="destination_address", + new_name="destination_addresses", + ), + migrations.RenameField( + model_name="policyrule", + old_name="source_address_group", + new_name="source_address_groups", + ), + migrations.RenameField( + model_name="policyrule", + old_name="source_address", + new_name="source_addresses", + ), + migrations.RenameField( + model_name="policyrule", + old_name="source_user_group", + new_name="source_user_groups", + ), + migrations.RenameField( + model_name="policyrule", + old_name="source_user", + new_name="source_users", + ), + migrations.RemoveField( + model_name="policyrule", + name="service", + ), + migrations.RemoveField( + model_name="policyrule", + name="service_group", + ), + migrations.AddField( + model_name="policyrule", + name="destination_service_groups", + field=models.ManyToManyField( + related_name="destination_policy_rules", + through="nautobot_firewall_models.DestSvcGroupM2M", + to="nautobot_firewall_models.ServiceObjectGroup", + ), + ), + migrations.AddField( + model_name="policyrule", + name="destination_services", + field=models.ManyToManyField( + related_name="destination_policy_rules", + through="nautobot_firewall_models.DestSvcM2M", + to="nautobot_firewall_models.ServiceObject", + ), + ), + migrations.AddField( + model_name="policyrule", + name="index", + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.CreateModel( + name="SrcSvcM2M", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ( + "pol_rule", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="nautobot_firewall_models.policyrule" + ), + ), + ( + "svc", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="nautobot_firewall_models.serviceobject" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="SrcSvcGroupM2M", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ( + "pol_rule", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="nautobot_firewall_models.policyrule" + ), + ), + ( + "svc_group", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="nautobot_firewall_models.serviceobjectgroup" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="policyrule", + name="source_service_groups", + field=models.ManyToManyField( + related_name="source_policy_rules", + through="nautobot_firewall_models.SrcSvcGroupM2M", + to="nautobot_firewall_models.ServiceObjectGroup", + ), + ), + migrations.AddField( + model_name="policyrule", + name="source_services", + field=models.ManyToManyField( + related_name="source_policy_rules", + through="nautobot_firewall_models.SrcSvcM2M", + to="nautobot_firewall_models.ServiceObject", + ), + ), + ] diff --git a/nautobot_firewall_models/migrations/0007_renaming_part2.py b/nautobot_firewall_models/migrations/0007_renaming_part2.py new file mode 100644 index 00000000..0753e45c --- /dev/null +++ b/nautobot_firewall_models/migrations/0007_renaming_part2.py @@ -0,0 +1,55 @@ +"""Custom migration for moving index to the PolicyRule.""" +from django.db import migrations +from django.db.models import Count + + +def move_index(apps, schedma_editor): + """Custom migration for moving index to the PolicyRule.""" + # Get models to work with + PolicyRuleM2M = apps.get_model("nautobot_firewall_models.PolicyRuleM2M") + PolicyRule = apps.get_model("nautobot_firewall_models.PolicyRule") + # Get list of duplicates + duplicates = PolicyRuleM2M.objects.values("rule").annotate(Count("id")).filter(id__count__gt=1) + duplicates = [i["rule"] for i in duplicates] + # Create Rules for Duplicates + for rule in PolicyRuleM2M.objects.filter(rule__in=duplicates): + source_users = rule.rule.source_users.all() + source_user_groups = rule.rule.source_user_groups.all() + source_addresses = rule.rule.source_addresses.all() + source_address_groups = rule.rule.source_address_groups.all() + destination_addresses = rule.rule.destination_addresses.all() + destination_address_groups = rule.rule.destination_address_groups.all() + destination_services = rule.rule.destination_services.all() + destination_service_groups = rule.rule.destination_service_groups.all() + temp_rule = rule.rule + temp_rule.id = None + temp_rule.name = f"{rule.rule.name} for {rule.policy.name}" + temp_rule.index = rule.index + temp_rule.save() + temp_rule.source_users.set(source_users) + temp_rule.source_user_groups.set(source_user_groups) + temp_rule.source_addresses.set(source_addresses) + temp_rule.source_address_groups.set(source_address_groups) + temp_rule.destination_addresses.set(destination_addresses) + temp_rule.destination_address_groups.set(destination_address_groups) + temp_rule.destination_services.set(destination_services) + temp_rule.destination_service_groups.set(destination_service_groups) + rule.rule = temp_rule + rule.save() + # Move Indexes for non-duplicates + for rule in PolicyRuleM2M.objects.exclude(rule__in=duplicates): + rule.rule.index = rule.index + rule.rule.save() + # Remove the duplicates + PolicyRule.objects.filter(id__in=duplicates).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("nautobot_firewall_models", "0006_renaming_part1"), + ] + + operations = [ + migrations.RunPython(code=move_index), + ] diff --git a/nautobot_firewall_models/migrations/0008_renaming_part3.py b/nautobot_firewall_models/migrations/0008_renaming_part3.py new file mode 100644 index 00000000..73ff8283 --- /dev/null +++ b/nautobot_firewall_models/migrations/0008_renaming_part3.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.15 on 2022-08-27 23:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("nautobot_firewall_models", "0007_renaming_part2"), + ] + + operations = [ + migrations.AlterModelOptions( + name="policyrule", + options={"ordering": ["index"], "verbose_name_plural": "Policy Rules"}, + ), + migrations.AlterModelOptions( + name="policyrulem2m", + options={}, + ), + migrations.RemoveConstraint( + model_name="policyrulem2m", + name="unique_with_index", + ), + migrations.RemoveConstraint( + model_name="policyrulem2m", + name="unique_without_index", + ), + migrations.RemoveField( + model_name="policyrulem2m", + name="index", + ), + ] diff --git a/nautobot_firewall_models/models/__init__.py b/nautobot_firewall_models/models/__init__.py index fe7d4a49..f7b65c64 100644 --- a/nautobot_firewall_models/models/__init__.py +++ b/nautobot_firewall_models/models/__init__.py @@ -17,6 +17,8 @@ AddressObjectGroupM2M, DestAddrGroupM2M, DestAddrM2M, + DestSvcGroupM2M, + DestSvcM2M, FQDNIPAddressM2M, PolicyRuleM2M, PolicyDeviceM2M, @@ -26,8 +28,8 @@ SrcAddrM2M, SrcUserGroupM2M, SrcUserM2M, - SvcGroupM2M, - SvcM2M, + SrcSvcGroupM2M, + SrcSvcM2M, UserObjectGroupM2M, ZoneInterfaceM2M, ZoneVRFM2M, @@ -44,6 +46,8 @@ "CapircaPolicy", "DestAddrGroupM2M", "DestAddrM2M", + "DestSvcGroupM2M", + "DestSvcM2M", "FQDN", "FQDNIPAddressM2M", "IPRange", @@ -59,8 +63,8 @@ "SrcAddrM2M", "SrcUserGroupM2M", "SrcUserM2M", - "SvcGroupM2M", - "SvcM2M", + "SrcSvcGroupM2M", + "SrcSvcM2M", "UserObject", "UserObjectGroup", "UserObjectGroupM2M", diff --git a/nautobot_firewall_models/models/core_models.py b/nautobot_firewall_models/models/core_models.py index 14f3711c..e814fd00 100644 --- a/nautobot_firewall_models/models/core_models.py +++ b/nautobot_firewall_models/models/core_models.py @@ -500,28 +500,38 @@ class PolicyRule(PrimaryModel): name = models.CharField(max_length=100) tags = TaggableManager(through=TaggedItem) - source_user = models.ManyToManyField(to=UserObject, through="SrcUserM2M", related_name="policy_rules") - source_user_group = models.ManyToManyField( + source_users = models.ManyToManyField(to=UserObject, through="SrcUserM2M", related_name="policy_rules") + source_user_groups = models.ManyToManyField( to=UserObjectGroup, through="SrcUserGroupM2M", related_name="policy_rules" ) - source_address = models.ManyToManyField(to=AddressObject, through="SrcAddrM2M", related_name="source_policy_rules") - source_address_group = models.ManyToManyField( + source_addresses = models.ManyToManyField( + to=AddressObject, through="SrcAddrM2M", related_name="source_policy_rules" + ) + source_address_groups = models.ManyToManyField( to=AddressObjectGroup, through="SrcAddrGroupM2M", related_name="source_policy_rules" ) source_zone = models.ForeignKey( to=Zone, null=True, blank=True, on_delete=models.SET_NULL, related_name="source_policy_rules" ) - destination_address = models.ManyToManyField( + source_services = models.ManyToManyField(to=ServiceObject, through="SrcSvcM2M", related_name="source_policy_rules") + source_service_groups = models.ManyToManyField( + to=ServiceObjectGroup, through="SrcSvcGroupM2M", related_name="source_policy_rules" + ) + destination_addresses = models.ManyToManyField( to=AddressObject, through="DestAddrM2M", related_name="destination_policy_rules" ) - destination_address_group = models.ManyToManyField( + destination_address_groups = models.ManyToManyField( to=AddressObjectGroup, through="DestAddrGroupM2M", related_name="destination_policy_rules" ) destination_zone = models.ForeignKey( to=Zone, on_delete=models.SET_NULL, null=True, blank=True, related_name="destination_policy_rules" ) - service = models.ManyToManyField(to=ServiceObject, through="SvcM2M", related_name="policy_rules") - service_group = models.ManyToManyField(to=ServiceObjectGroup, through="SvcGroupM2M", related_name="policy_rules") + destination_services = models.ManyToManyField( + to=ServiceObject, through="DestSvcM2M", related_name="destination_policy_rules" + ) + destination_service_groups = models.ManyToManyField( + to=ServiceObjectGroup, through="DestSvcGroupM2M", related_name="destination_policy_rules" + ) action = models.CharField(choices=choices.ACTION_CHOICES, max_length=20) log = models.BooleanField(default=False) status = StatusField( @@ -531,11 +541,12 @@ class PolicyRule(PrimaryModel): ) request_id = models.CharField(max_length=100, null=True, blank=True) description = models.CharField(max_length=200, null=True, blank=True) + index = models.PositiveSmallIntegerField(null=True, blank=True) class Meta: """Meta class.""" - ordering = ["name"] + ordering = ["index"] verbose_name_plural = "Policy Rules" def get_absolute_url(self): @@ -546,18 +557,19 @@ def rule_details(self): """Convience method to convert to more consumable dictionary.""" row = {} row["rule"] = self - row["source_address_group"] = self.source_address_group.all() - row["source_address"] = self.source_address.all() - row["source_user"] = self.source_user.all() - row["source_user_group"] = self.source_user_group.all() + row["source_address_groups"] = self.source_address_groups.all() + row["source_addresses"] = self.source_addresses.all() + row["source_users"] = self.source_users.all() + row["source_user_groupes"] = self.source_user_groups.all() row["source_zone"] = self.source_zone + row["source_services"] = self.source_services.all() + row["source_service_groups"] = self.source_service_groups.all() - row["destination_address_group"] = self.destination_address_group.all() - row["destination_address"] = self.destination_address.all() + row["destination_address_groups"] = self.destination_address_groups.all() + row["destination_addresses"] = self.destination_addresses.all() row["destination_zone"] = self.destination_zone - - row["service"] = self.service.all() - row["service_group"] = self.service_group.all() + row["destination_services"] = self.destination_services.all() + row["destination_service_groups"] = self.destination_service_groups.all() row["action"] = self.action row["log"] = self.log diff --git a/nautobot_firewall_models/models/through_models.py b/nautobot_firewall_models/models/through_models.py index 61ea1ffc..2a7edb1a 100644 --- a/nautobot_firewall_models/models/through_models.py +++ b/nautobot_firewall_models/models/through_models.py @@ -1,8 +1,6 @@ """Set of through intermediate models.""" from django.db import models -from django.db.models import Q -from django.db.models.constraints import UniqueConstraint from nautobot.core.models.generics import BaseModel @@ -63,20 +61,10 @@ class Meta: class PolicyRuleM2M(BaseModel): - """Through model to add index to the the Policy & PolicyRule relationship.""" + """Custom through model to on_delete=models.PROTECT to prevent deleting associated PolicyRule if assigned to a Policy.""" policy = models.ForeignKey("nautobot_firewall_models.Policy", on_delete=models.CASCADE) rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.PROTECT) - index = models.PositiveSmallIntegerField(null=True, blank=True) - - class Meta: - """Meta class.""" - - ordering = ["index"] - constraints = [ - UniqueConstraint(fields=["policy", "rule", "index"], name="unique_with_index"), - UniqueConstraint(fields=["policy", "rule"], name="unique_without_index", condition=Q(index=None)), - ] class ServiceObjectGroupM2M(BaseModel): @@ -114,14 +102,28 @@ class SrcUserGroupM2M(BaseModel): pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) -class SvcM2M(BaseModel): +class SrcSvcM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated Service if assigned to a PolicyRule.""" + + svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class SrcSvcGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated ServiceGroup if assigned to a PolicyRule.""" + + svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class DestSvcM2M(BaseModel): """Custom through model to on_delete=models.PROTECT to prevent deleting associated Service if assigned to a PolicyRule.""" svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) -class SvcGroupM2M(BaseModel): +class DestSvcGroupM2M(BaseModel): """Custom through model to on_delete=models.PROTECT to prevent deleting associated ServiceGroup if assigned to a PolicyRule.""" svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) diff --git a/nautobot_firewall_models/navigation.py b/nautobot_firewall_models/navigation.py index b72ae895..8e24d45f 100644 --- a/nautobot_firewall_models/navigation.py +++ b/nautobot_firewall_models/navigation.py @@ -4,7 +4,7 @@ menu_items = ( NavMenuTab( - name="Firewall", + name="Security", # weight=150, groups=[ NavMenuGroup( diff --git a/nautobot_firewall_models/tables.py b/nautobot_firewall_models/tables.py index 8b5faa42..7e2f46a7 100644 --- a/nautobot_firewall_models/tables.py +++ b/nautobot_firewall_models/tables.py @@ -150,16 +150,18 @@ class Meta(BaseTable.Meta): # pylint: disable=duplicate-code "pk", "name", - "source_user", - "source_user_group", - "source_address", - "source_address_group", + "source_users", + "source_user_groups", + "source_addresses", + "source_address_groups", "source_zone", - "destination_address", - "destination_address_group", + "source_services", + "source_service_groups", + "destination_addresses", + "destination_address_groups", "destination_zone", - "service", - "service_group", + "destination_services", + "destination_service_groups", "action", "log", "status", diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/assign_policy_rule_index.html b/nautobot_firewall_models/templates/nautobot_firewall_models/assign_policy_rule_index.html deleted file mode 100644 index b717f248..00000000 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/assign_policy_rule_index.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load buttons %} -{% load static %} -{% load custom_links %} -{% load helpers %} -{% load plugins %} - -
-
-
-
- Assign Policy Rule Index -
-
- {% csrf_token %} - - {% include 'nautobot_firewall_models/inc/policyrule_tablehead.html' with parent_policy=True %} - {% for m2m in object.policyrulem2m_set.all %} - {% include 'nautobot_firewall_models/inc/policy_rules_tablerow_edit.html' with m2m=m2m parent_policy=True %} - {% endfor %} -
- -
-
-
-
\ No newline at end of file diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/policy_expanded_rules.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_expanded_rules.html similarity index 100% rename from nautobot_firewall_models/templates/nautobot_firewall_models/policy_expanded_rules.html rename to nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_expanded_rules.html diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_address_object_row.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_address_object_row.html index c08b460b..f642f1ce 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_address_object_row.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_address_object_row.html @@ -1,15 +1,11 @@ {% load helpers %} -{% if address_group %} +{% if address_group or address %} {% for i in address_group %} {{ i|placeholder }}
{% endfor %} -{% else %} - {{ None|placeholder }} -{% endif %} -{% if address %} {% for i in address %} {{ i|placeholder }}
{% endfor %} {% else %} - {{ None|placeholder }} + ANY {% endif %} diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_service_object_row.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_service_object_row.html index 144a194e..d092bb4f 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_service_object_row.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_service_object_row.html @@ -1,16 +1,12 @@ {% load helpers %} -{% if service_group %} +{% if service_group or service %} {% for i in service_group %} {{ i|placeholder }}
{% endfor %} -{% else %} - {{ None|placeholder }} -{% endif %} -{% if service %} {% for i in service %} {{ i|placeholder }}
{% endfor %} {% else %} - {{ None|placeholder }} -{% endif %} + ANY + {% endif %} diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_user_object_row.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_user_object_row.html index cddf3e8e..a944b2c7 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_user_object_row.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_user_object_row.html @@ -1,16 +1,12 @@ {% load helpers %} -{% if user_group %} +{% if user_group or user %} {% for i in user_group %} {{ i|placeholder }}
{% endfor %} -{% else %} - {{ None|placeholder }} -{% endif %} -{% if user %} {% for i in user %} {{ i|placeholder }}
{% endfor %} {% else %} - {{ None|placeholder }} +ANY {% endif %} \ No newline at end of file diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_zone_object_row.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_zone_object_row.html index 0dcd5c55..f6985f21 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_zone_object_row.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_zone_object_row.html @@ -3,5 +3,5 @@ {% if zone %} {{ zone|placeholder }}
{% else %} - {{ None|placeholder }} + ANY {% endif %} \ No newline at end of file diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rules_tablerow.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rules_tablerow.html index 792543af..b7c77864 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rules_tablerow.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rules_tablerow.html @@ -1,15 +1,16 @@ {% load helpers %} - {{ m2m.index|placeholder|ljust:5 }} → {{ m2m.rule.name|placeholder }} + {{ m2m.rule.index|placeholder|ljust:5 }} → {{ m2m.rule.name|placeholder }} {% if m2m.rule.action == "remark" %} - {{ m2m.rule }} + {{ m2m.rule }} {% else %} - {% include './policy_rule_address_object_row.html' with address=m2m.rule.source_address.all address_group=m2m.rule.source_address_group.all %} - {% include './policy_rule_user_object_row.html' with user=m2m.rule.source_user.all user_group=m2m.rule.source_user_group.all %} + {% include './policy_rule_address_object_row.html' with address=m2m.rule.source_addresses.all address_group=m2m.rule.source_address_groups.all %} + {% include './policy_rule_user_object_row.html' with user=m2m.rule.source_users.all user_group=m2m.rule.source_user_groups.all %} {% include './policy_rule_zone_object_row.html' with zone=m2m.rule.source_zone %} - {% include './policy_rule_address_object_row.html' with address=m2m.rule.destination_address.all address_group=m2m.rule.destination_address_group.all %} + {% include './policy_rule_service_object_row.html' with service=m2m.rule.source_services.all service_group=m2m.rule.source_service_groups.all %} + {% include './policy_rule_address_object_row.html' with address=m2m.rule.destination_addresses.all address_group=m2m.rule.destination_address_groups.all %} {% include './policy_rule_zone_object_row.html' with zone=m2m.rule.destination_zone %} - {% include './policy_rule_service_object_row.html' with service=m2m.rule.service.all service_group=m2m.rule.service_group.all %} + {% include './policy_rule_service_object_row.html' with service=m2m.rule.destination_services.all service_group=m2m.rule.destination_service_groups.all %} {% endif %} {% include './policy_rule_action_row.html' with action=m2m.rule.action %} diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rules_tablerow_edit.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rules_tablerow_edit.html deleted file mode 100644 index fdbbf416..00000000 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rules_tablerow_edit.html +++ /dev/null @@ -1,16 +0,0 @@ -{% load helpers %} - - - {% if m2m.rule.action == "remark" %} - {{ m2m.rule.name }} - {% else %} - {% include './policy_rule_address_object_row.html' with address=m2m.rule.source_address.all address_group=m2m.rule.source_address_group.all %} - {% include './policy_rule_user_object_row.html' with user=m2m.rule.source_user.all user_group=m2m.rule.source_user_group.all %} - {% include './policy_rule_zone_object_row.html' with zone=m2m.rule.source_zone %} - {% include './policy_rule_address_object_row.html' with address=m2m.rule.destination_address.all address_group=m2m.rule.destination_address_group.all %} - {% include './policy_rule_zone_object_row.html' with zone=m2m.rule.destination_zone %} - {% include './policy_rule_service_object_row.html' with service=m2m.rule.service.all service_group=m2m.rule.service_group.all %} - {% endif %} - {{ m2m.rule.action|placeholder }} - {% if m2m.rule.log %}{% else %}{% endif %} - diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablehead.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablehead.html index 358be0c9..50a6c8f9 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablehead.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablehead.html @@ -1,15 +1,16 @@ - {% if parent_policy %}Index{% endif %} - Source - Destination - Action - Log + Index + Source + Destination + Action + Log - Address - User - Zone - Address - Zone - Service + Address + User + Zone + Service + Address + Zone + Service \ No newline at end of file diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablerow.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablerow.html index a7dd9682..9d6083aa 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablerow.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablerow.html @@ -1,14 +1,16 @@ {% load helpers %} + {{ rule.index }} {% if rule.action == "remark" %} - {{ rule }} + {{ rule }} {% else %} - {% include './policy_rule_address_object_row.html' with address=rule.source_address.all address_group=rule.source_address_group.all %} - {% include './policy_rule_user_object_row.html' with user=rule.source_user.all user_group=rule.source_user_group.all %} + {% include './policy_rule_address_object_row.html' with address=rule.source_addresses.all address_group=rule.source_address_groups.all %} + {% include './policy_rule_user_object_row.html' with user=rule.source_users.all user_group=rule.source_user_groups.all %} {% include './policy_rule_zone_object_row.html' with zone=rule.source_zone %} - {% include './policy_rule_address_object_row.html' with address=rule.destination_address.all address_group=rule.destination_address_group.all %} + {% include './policy_rule_service_object_row.html' with service=rule.source_services.all service_group=rule.source_service_groups.all %} + {% include './policy_rule_address_object_row.html' with address=rule.destination_addresses.all address_group=rule.destination_address_groups.all %} {% include './policy_rule_zone_object_row.html' with zone=rule.destination_zone %} - {% include './policy_rule_service_object_row.html' with service=rule.service.all service_group=rule.service_group.all %} + {% include './policy_rule_service_object_row.html' with service=rule.destination_services.all service_group=rule.destination_service_groups.all %} {% endif %} {% include './policy_rule_action_row.html' with action=rule.action %} diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/policy.html b/nautobot_firewall_models/templates/nautobot_firewall_models/policy.html index 030c14bd..807e71cd 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/policy.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/policy.html @@ -14,13 +14,6 @@ Policy Rules Expanded - {% if perms.nautobot_firewall_models.edit_policy %} - - {% endif %} {% if perms.nautobot_firewall_models.edit_policy %}