diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ae9729..14d1a052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## v1.2.0-alpha.0 - 2022-11-22 + +### Added + +- #108 Application & ApplicationGroup support + +### Fixed + +- #117 Resolved `main` tab not loading on initial load of Policy & PolicyRule detail page + +### Changed + +- #117 Reorganized `models/` folder to improve developer experience + ## v1.1.3 - 2022-11-16 ### Fixed diff --git a/docs/example.md b/docs/example.md index 3ed820dd..c785f561 100644 --- a/docs/example.md +++ b/docs/example.md @@ -245,4 +245,4 @@ Below is an example response to the REST API GET request for a Policy object at "status": "35206353-47f4-4e71-9e2c-807092b6c439", "tenant": "5fabe6c7-84a6-45af-95a0-384f9ebcbeb8" } -``` \ No newline at end of file +``` diff --git a/docs/images/policy.png b/docs/images/policy.png index 6d8d3e71..62219e1c 100644 Binary files a/docs/images/policy.png and b/docs/images/policy.png differ diff --git a/docs/models.md b/docs/models.md index b2e21580..2ed8d8e5 100644 --- a/docs/models.md +++ b/docs/models.md @@ -96,6 +96,16 @@ This plugin uses [custom models to model many-to-many](https://docs.djangoprojec heading-offset=2 %} +{% + include-markdown "./models/applicationobject.md" + heading-offset=2 +%} + +{% + include-markdown "./models/applicationobjectgroup.md" + heading-offset=2 +%} + {% include-markdown "./models/fqdn.md" heading-offset=2 diff --git a/docs/models/applicationobject.md b/docs/models/applicationobject.md new file mode 100644 index 00000000..490c77ef --- /dev/null +++ b/docs/models/applicationobject.md @@ -0,0 +1,15 @@ +# ApplicationObject + +Defines an Applications that are typically referenced in NextGen Firewalls. + +## Attributes + +* Name (string) +* Description (optional, string) +* Category (optional, string) +* Subategory (optional, string) +* Technology (optional, string) +* Risk (optional, positive integer) +* Default Type (optional, int OR int range OR comma delimited list of int or int range) +* Default IP Protocol (optional, string) +* Status (FK to Status) diff --git a/docs/models/applicationobjectgroup.md b/docs/models/applicationobjectgroup.md new file mode 100644 index 00000000..94d51fa4 --- /dev/null +++ b/docs/models/applicationobjectgroup.md @@ -0,0 +1,10 @@ +# ApplicationObjectGroup + +Represents a group of Application Objects. + +## Attributes + +* Name (string) +* Description (optional, string) +* Application Objects (M2M to ApplicationObject) +* Status (FK to Status) diff --git a/docs/models/serviceobject.md b/docs/models/serviceobject.md index 7dc4a653..a7a62c13 100644 --- a/docs/models/serviceobject.md +++ b/docs/models/serviceobject.md @@ -8,7 +8,7 @@ For well-known ports, it is best to use the port name as the name of the object. * Name (string) * Description (optional, string) -* Port (optional, int OR int range) +* Port (optional, int OR int range OR comma delimited list of int or int range) * Must be specified as a valid layer 4 port OR port range (e.g. 80 OR 8080-8088). * IP Protocol (string, choice field) * IANA protocols (e.g. TCP UDP ICMP) diff --git a/nautobot_firewall_models/api/nested_serializers.py b/nautobot_firewall_models/api/nested_serializers.py index 4342668d..7f6d7845 100644 --- a/nautobot_firewall_models/api/nested_serializers.py +++ b/nautobot_firewall_models/api/nested_serializers.py @@ -5,6 +5,18 @@ from nautobot_firewall_models import models +class NestedApplicationSerializer(WritableNestedSerializer): + """Nested serializer for FQDN.""" + + url = HyperlinkedIdentityField(view_name="plugins-api:nautobot_firewall_models-api:applicationobject-detail") + + class Meta: + """Meta attributes.""" + + model = models.FQDN + fields = ["id", "url", "name"] + + class NestedFQDNSerializer(WritableNestedSerializer): """Nested serializer for FQDN.""" diff --git a/nautobot_firewall_models/api/serializers.py b/nautobot_firewall_models/api/serializers.py index eed2d251..5d1f3483 100644 --- a/nautobot_firewall_models/api/serializers.py +++ b/nautobot_firewall_models/api/serializers.py @@ -18,7 +18,11 @@ from nautobot_firewall_models import models -from nautobot_firewall_models.api.nested_serializers import NestedFQDNSerializer, NestedIPRangeSerializer +from nautobot_firewall_models.api.nested_serializers import ( + NestedFQDNSerializer, + NestedIPRangeSerializer, + # NestedApplicationSerializer, +) class StatusModelSerializerMixin(_StatusModelSerializerMixin): # pylint: disable=abstract-method @@ -95,6 +99,40 @@ class Meta: fields = "__all__" +class ApplicationObjectSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): + """ApplicationObject Serializer.""" + + url = serializers.HyperlinkedIdentityField( + view_name="plugins-api:nautobot_firewall_models-api:applicationobject-detail" + ) + + class Meta: + """Meta attributes.""" + + model = models.ApplicationObject + fields = "__all__" + + +class ApplicationObjectGroupSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): + """ApplicationObjectGroup Serializer.""" + + url = serializers.HyperlinkedIdentityField( + view_name="plugins-api:nautobot_firewall_models-api:applicationobject-detail" + ) + application_objects = SerializedPKRelatedField( + queryset=models.ApplicationObject.objects.all(), + serializer=ApplicationObjectSerializer, + required=False, + many=True, + ) + + class Meta: + """Meta attributes.""" + + model = models.ApplicationObjectGroup + fields = "__all__" + + class ServiceObjectSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, NautobotModelSerializer): """ServiceObject Serializer.""" @@ -209,6 +247,18 @@ class PolicyRuleSerializer(TaggedObjectSerializer, StatusModelSerializerMixin, N required=False, many=True, ) + applications = SerializedPKRelatedField( + queryset=models.ApplicationObject.objects.all(), + serializer=ApplicationObjectSerializer, + required=False, + many=True, + ) + application_groups = SerializedPKRelatedField( + queryset=models.ApplicationObjectGroup.objects.all(), + serializer=ApplicationObjectGroupSerializer, + required=False, + many=True, + ) class Meta: """Meta attributes.""" diff --git a/nautobot_firewall_models/api/urls.py b/nautobot_firewall_models/api/urls.py index 7defe02d..e6e06fe0 100644 --- a/nautobot_firewall_models/api/urls.py +++ b/nautobot_firewall_models/api/urls.py @@ -8,6 +8,8 @@ router = OrderedDefaultRouter() router.register("address-object", views.AddressObjectViewSet) router.register("address-object-group", views.AddressObjectGroupViewSet) +router.register("application-object", views.ApplicationObjectViewSet) +router.register("application-object-group", views.ApplicationObjectGroupViewSet) router.register("capirca-policy", views.CapircaPolicyViewSet) router.register("fqdn", views.FQDNViewSet) router.register("ip-range", views.IPRangeViewSet) diff --git a/nautobot_firewall_models/api/views.py b/nautobot_firewall_models/api/views.py index a3f9f0e2..122a5078 100644 --- a/nautobot_firewall_models/api/views.py +++ b/nautobot_firewall_models/api/views.py @@ -40,6 +40,22 @@ class AddressObjectGroupViewSet(NautobotModelViewSet): filterset_class = filters.AddressObjectGroupFilterSet +class ApplicationObjectViewSet(NautobotModelViewSet): + """ApplicationObject viewset.""" + + queryset = models.ApplicationObject.objects.all() + serializer_class = serializers.ApplicationObjectSerializer + filterset_class = filters.ApplicationObjectFilterSet + + +class ApplicationObjectGroupViewSet(NautobotModelViewSet): + """ApplicationObjectGroup viewset.""" + + queryset = models.ApplicationObjectGroup.objects.all() + serializer_class = serializers.ApplicationObjectGroupSerializer + filterset_class = filters.ApplicationObjectGroupFilterSet + + class ServiceObjectViewSet(NautobotModelViewSet): """ServiceObject viewset.""" diff --git a/nautobot_firewall_models/filters.py b/nautobot_firewall_models/filters.py index bb194f54..dcd1973d 100644 --- a/nautobot_firewall_models/filters.py +++ b/nautobot_firewall_models/filters.py @@ -77,6 +77,28 @@ class Meta: fields = ["id", "name", "address_objects", "description"] +class ApplicationObjectFilterSet(BaseFilterSet, NautobotFilterSet): + """Filter for ApplicationObject.""" + + class Meta: + """Meta attributes for filter.""" + + model = models.ApplicationObject + + fields = ["id", "name", "description", "category", "subcategory", "risk", "description"] + + +class ApplicationObjectGroupFilterSet(BaseFilterSet, NautobotFilterSet): + """Filter for ApplicationObjectGroup.""" + + class Meta: + """Meta attributes for filter.""" + + model = models.ApplicationObjectGroup + + fields = ["id", "name", "application_objects", "description"] + + class ServiceObjectFilterSet(BaseFilterSet, NautobotFilterSet): """Filter for ServiceObject.""" diff --git a/nautobot_firewall_models/forms.py b/nautobot_firewall_models/forms.py index b9032a86..a55b677c 100644 --- a/nautobot_firewall_models/forms.py +++ b/nautobot_firewall_models/forms.py @@ -197,6 +197,110 @@ class Meta: ] +class ApplicationObjectFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): + """Filter form to filter searches.""" + + field_order = ["q", "name"] + + model = models.ApplicationObject + q = forms.CharField( + required=False, + label="Search", + help_text="Search within Name or Description.", + ) + name = forms.CharField(required=False, label="Name") + category = DynamicModelChoiceField( + queryset=models.ApplicationObject.objects.all(), required=False, label="Category" + ) + + +class ApplicationObjectForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): + """ApplicationObject creation/edit form.""" + + class Meta: + """Meta attributes.""" + + model = models.ApplicationObject + fields = [ + "name", + "description", + "category", + "subcategory", + "technology", + "risk", + "default_type", + "default_ip_protocol", + "status", + ] + + +class ApplicationObjectBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): + """ApplicationObject bulk edit form.""" + + pk = DynamicModelMultipleChoiceField( + queryset=models.ApplicationObject.objects.all(), widget=forms.MultipleHiddenInput + ) + description = forms.CharField(required=False) + risk = forms.IntegerField(required=False) + technology = forms.CharField(required=False) + category = forms.CharField(required=False) + subcategory = forms.CharField(required=False) + + class Meta: + """Meta attributes.""" + + nullable_fields = [ + "description", + "default_ip_protocol", + "default_type", + "technology", + "category", + "subcategory", + ] + + +class ApplicationObjectGroupFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): + """Filter form to filter searches.""" + + field_order = ["q", "name"] + + model = models.ApplicationObjectGroup + q = forms.CharField( + required=False, + label="Search", + help_text="Search within Name or Description.", + ) + name = forms.CharField(required=False, label="Name") + + +class ApplicationObjectGroupForm(BootstrapMixin, RelationshipModelFormMixin, forms.ModelForm): + """ApplicationObjectGroup creation/edit form.""" + + application_objects = DynamicModelMultipleChoiceField(queryset=models.ApplicationObject.objects.all()) + + class Meta: + """Meta attributes.""" + + model = models.ApplicationObjectGroup + fields = ["name", "description", "application_objects", "status", "tags"] + + +class ApplicationObjectGroupBulkEditForm(BootstrapMixin, StatusModelBulkEditFormMixin, BulkEditForm): + """ApplicationObjectGroup bulk edit form.""" + + pk = DynamicModelMultipleChoiceField( + queryset=models.ApplicationObjectGroup.objects.all(), widget=forms.MultipleHiddenInput + ) + description = forms.CharField(required=False) + + class Meta: + """Meta attributes.""" + + nullable_fields = [ + "description", + ] + + class ServiceObjectFilterForm(BootstrapMixin, StatusModelFilterFormMixin, CustomFieldModelFilterFormMixin): """Filter form to filter searches.""" @@ -473,6 +577,14 @@ class PolicyRuleForm(BootstrapMixin, CustomFieldModelFormMixin, RelationshipMode destination_service_groups = DynamicModelMultipleChoiceField( queryset=models.ServiceObjectGroup.objects.all(), label="Destination Service Object Groups", required=False ) + applications = DynamicModelMultipleChoiceField( + queryset=models.ApplicationObject.objects.all(), label="Destination Application Objects", required=False + ) + application_groups = DynamicModelMultipleChoiceField( + queryset=models.ApplicationObjectGroup.objects.all(), + label="Destination Application Object Groups", + required=False, + ) request_id = forms.CharField(required=False, label="Optional field for request ticket identifier.") class Meta: @@ -495,6 +607,8 @@ class Meta: "destination_zone", "destination_services", "destination_service_groups", + "applications", + "application_groups", "action", "log", "status", diff --git a/nautobot_firewall_models/homepage.py b/nautobot_firewall_models/homepage.py index ce4abeda..b38407a2 100644 --- a/nautobot_firewall_models/homepage.py +++ b/nautobot_firewall_models/homepage.py @@ -9,45 +9,45 @@ name="Security", items=( HomePageItem( - name="Policies", + name="Security Policies", model=Policy, weight=100, link="plugins:nautobot_firewall_models:policy_list", description="Firewall Policies", permissions=["nautobot_firewall_models.view_policy"], ), - HomePageItem( - name="Policy Rules", - model=PolicyRule, - weight=200, - link="plugins:nautobot_firewall_models:policyrule_list", - description="Firewall Policies", - permissions=["nautobot_firewall_models.view_policyrule"], - ), HomePageItem( name="NAT Policies", model=NATPolicy, - weight=300, + weight=200, link="plugins:nautobot_firewall_models:natpolicy_list", description="NAT Policies", permissions=["nautobot_firewall_models.view_natpolicy"], ), - HomePageItem( - name="NAT Policy Rules", - model=NATPolicyRule, - weight=400, - link="plugins:nautobot_firewall_models:natpolicyrule_list", - description="NAT Policies", - permissions=["nautobot_firewall_models.view_natpolicyrule"], - ), HomePageItem( name="Capirca Policies", model=CapircaPolicy, - weight=150, + weight=300, link="plugins:nautobot_firewall_models:capircapolicy_list", description="Firewall Policies", permissions=["nautobot_firewall_models.view_capircapolicy"], ), + HomePageItem( + name="Security Rules", + model=PolicyRule, + weight=400, + link="plugins:nautobot_firewall_models:policyrule_list", + description="Firewall Policies", + permissions=["nautobot_firewall_models.view_policyrule"], + ), + HomePageItem( + name="NAT Rules", + model=NATPolicyRule, + weight=500, + link="plugins:nautobot_firewall_models:natpolicyrule_list", + description="NAT Policies", + permissions=["nautobot_firewall_models.view_natpolicyrule"], + ), ), ), ) diff --git a/nautobot_firewall_models/jobs.py b/nautobot_firewall_models/jobs.py index 9025e6e3..79928b72 100644 --- a/nautobot_firewall_models/jobs.py +++ b/nautobot_firewall_models/jobs.py @@ -5,7 +5,7 @@ from nautobot.dcim.models import Device from nautobot_firewall_models.models import CapircaPolicy -from nautobot_firewall_models.models.core_models import Policy +from nautobot_firewall_models.models import Policy LOGGER = logging.getLogger(__name__) diff --git a/nautobot_firewall_models/migrations/0013_applications.py b/nautobot_firewall_models/migrations/0013_applications.py new file mode 100644 index 00000000..3bb95ba6 --- /dev/null +++ b/nautobot_firewall_models/migrations/0013_applications.py @@ -0,0 +1,234 @@ +# Generated by Django 3.2.15 on 2022-11-22 21:23 + +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import nautobot.extras.models.mixins +import nautobot.extras.models.statuses +import nautobot_firewall_models.utils +import taggit.managers +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("extras", "0047_enforce_custom_field_slug"), + ("nautobot_firewall_models", "0012_remove_status_m2m_through_models"), + ] + + operations = [ + migrations.CreateModel( + name="ApplicationObject", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ("created", models.DateField(auto_now_add=True, null=True)), + ("last_updated", models.DateTimeField(auto_now=True, null=True)), + ( + "_custom_field_data", + models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + ("description", models.CharField(blank=True, max_length=200)), + ("category", models.CharField(blank=True, max_length=48)), + ("subcategory", models.CharField(blank=True, max_length=48)), + ("technology", models.CharField(blank=True, max_length=48)), + ("risk", models.PositiveIntegerField(blank=True)), + ("default_type", models.CharField(blank=True, max_length=48)), + ("name", models.CharField(max_length=100, unique=True)), + ("default_ip_protocol", models.CharField(blank=True, max_length=48)), + ( + "status", + nautobot.extras.models.statuses.StatusField( + default=nautobot_firewall_models.utils.get_default_status, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="nautobot_firewall_models_applicationobject_related", + to="extras.status", + ), + ), + ("tags", taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag")), + ], + options={ + "verbose_name_plural": "Application Objects", + "ordering": ["name"], + }, + bases=( + models.Model, + nautobot.extras.models.mixins.DynamicGroupMixin, + nautobot.extras.models.mixins.NotesMixin, + ), + ), + migrations.CreateModel( + name="ApplicationObjectGroup", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ("created", models.DateField(auto_now_add=True, null=True)), + ("last_updated", models.DateTimeField(auto_now=True, null=True)), + ( + "_custom_field_data", + models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + ("description", models.CharField(blank=True, max_length=200)), + ("name", models.CharField(max_length=100, unique=True)), + ], + options={ + "verbose_name_plural": "Application Object Groups", + "ordering": ["name"], + }, + bases=( + models.Model, + nautobot.extras.models.mixins.DynamicGroupMixin, + nautobot.extras.models.mixins.NotesMixin, + ), + ), + migrations.AlterModelOptions( + name="natpolicy", + options={"ordering": ["name"], "verbose_name": "NAT Policy", "verbose_name_plural": "NAT Policies"}, + ), + migrations.AlterModelOptions( + name="natpolicyrule", + options={ + "ordering": ["index"], + "verbose_name": "NAT Policy Rule", + "verbose_name_plural": "NAT Policy Rules", + }, + ), + migrations.AlterModelOptions( + name="iprange", + options={"ordering": ["start_address"], "verbose_name": "IP Range", "verbose_name_plural": "IP Ranges"}, + ), + migrations.CreateModel( + name="ApplicationObjectGroupM2M", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ( + "application", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="nautobot_firewall_models.applicationobject" + ), + ), + ( + "application_group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="nautobot_firewall_models.applicationobjectgroup", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="applicationobjectgroup", + name="application_objects", + field=models.ManyToManyField( + blank=True, + related_name="application_object_groups", + through="nautobot_firewall_models.ApplicationObjectGroupM2M", + to="nautobot_firewall_models.ApplicationObject", + ), + ), + migrations.AddField( + model_name="applicationobjectgroup", + name="status", + field=nautobot.extras.models.statuses.StatusField( + default=nautobot_firewall_models.utils.get_default_status, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="nautobot_firewall_models_applicationobjectgroup_related", + to="extras.status", + ), + ), + migrations.AddField( + model_name="applicationobjectgroup", + name="tags", + field=taggit.managers.TaggableManager(through="extras.TaggedItem", to="extras.Tag"), + ), + migrations.CreateModel( + name="ApplicationM2M", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ( + "app", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="nautobot_firewall_models.applicationobject" + ), + ), + ( + "pol_rule", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="nautobot_firewall_models.policyrule" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ApplicationGroupM2M", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ( + "app_group", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="nautobot_firewall_models.applicationobjectgroup", + ), + ), + ( + "pol_rule", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="nautobot_firewall_models.policyrule" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="policyrule", + name="application_groups", + field=models.ManyToManyField( + related_name="policy_rules", + through="nautobot_firewall_models.ApplicationGroupM2M", + to="nautobot_firewall_models.ApplicationObjectGroup", + ), + ), + migrations.AddField( + model_name="policyrule", + name="applications", + field=models.ManyToManyField( + related_name="policy_rules", + through="nautobot_firewall_models.ApplicationM2M", + to="nautobot_firewall_models.ApplicationObject", + ), + ), + ] diff --git a/nautobot_firewall_models/migrations/0014_custom_status_application.py b/nautobot_firewall_models/migrations/0014_custom_status_application.py new file mode 100644 index 00000000..e3459d58 --- /dev/null +++ b/nautobot_firewall_models/migrations/0014_custom_status_application.py @@ -0,0 +1,54 @@ +# Copied on 2022-11-18 + +from django.db import migrations + + +def create_app_status(apps, schema_editor): + """Initial subset of statuses for the Application models. + + This was added along with 0013_applications in order to associate the same set of statuses with the new Application models + that are associated with the original set of security models. + """ + + statuses = ["active", "staged", "decommissioned"] + ContentType = apps.get_model("contenttypes.ContentType") + relevant_models = [ + apps.get_model(model) + for model in ["nautobot_firewall_models.ApplicationObject", "nautobot_firewall_models.ApplicationObjectGroup"] + ] + for i in statuses: + status = apps.get_model("extras.Status").objects.get(slug=i) + for model in relevant_models: + ct = ContentType.objects.get_for_model(model) + status.content_types.add(ct) + + +def remove_app_status(apps, schema_editor): + """Remove status content_type for Application models. + + This was added along with 0013_applications in order to associate the same set of statuses with the new Application models + that are associated with the original set of security models. + """ + + statuses = ["active", "staged", "decommissioned"] + ContentType = apps.get_model("contenttypes.ContentType") + relevant_models = [ + apps.get_model(model) + for model in ["nautobot_firewall_models.ApplicationObject", "nautobot_firewall_models.ApplicationObjectGroup"] + ] + for i in statuses: + status = apps.get_model("extras.Status").objects.get(slug=i) + for model in relevant_models: + ct = ContentType.objects.get_for_model(model) + status.content_types.remove(ct) + + +class Migration(migrations.Migration): + + dependencies = [ + ("nautobot_firewall_models", "0013_applications"), + ] + + operations = [ + migrations.RunPython(code=create_app_status, reverse_code=remove_app_status), + ] diff --git a/nautobot_firewall_models/models/__init__.py b/nautobot_firewall_models/models/__init__.py index b30a4e79..1d7b523a 100644 --- a/nautobot_firewall_models/models/__init__.py +++ b/nautobot_firewall_models/models/__init__.py @@ -1,27 +1,19 @@ """Load models.""" -from .core_models import ( +from .address import ( AddressObject, AddressObjectGroup, + AddressObjectGroupM2M, FQDN, + FQDNIPAddressM2M, IPRange, +) +from .capirca_policy import ( + CapircaPolicy, +) +from .nat_policy import ( NATPolicy, NATPolicyRule, - Policy, - PolicyRule, - ServiceObject, - ServiceObjectGroup, - UserObject, - UserObjectGroup, - Zone, -) -from .through_models import ( - AddressObjectGroupM2M, - DestAddrGroupM2M, - DestAddrM2M, - DestSvcGroupM2M, - DestSvcM2M, - FQDNIPAddressM2M, NATOrigDestAddrGroupM2M, NATOrigDestAddrM2M, NATOrigDestSvcGroupM2M, @@ -41,29 +33,46 @@ NATTransSrcAddrM2M, NATTransSrcSvcGroupM2M, NATTransSrcSvcM2M, - PolicyRuleM2M, +) +from .security_policy import ( + DestAddrGroupM2M, + DestAddrM2M, + DestSvcGroupM2M, + DestSvcM2M, + Policy, PolicyDeviceM2M, PolicyDynamicGroupM2M, - ServiceObjectGroupM2M, + PolicyRule, + PolicyRuleM2M, SrcAddrGroupM2M, SrcAddrM2M, SrcUserGroupM2M, SrcUserM2M, SrcSvcGroupM2M, SrcSvcM2M, - UserObjectGroupM2M, +) +from .service import ( + ApplicationObject, + ApplicationObjectGroup, + ApplicationObjectGroupM2M, + ServiceObject, + ServiceObjectGroup, + ServiceObjectGroupM2M, +) +from .user import UserObject, UserObjectGroup, UserObjectGroupM2M +from .zone import ( + Zone, ZoneInterfaceM2M, ZoneVRFM2M, ) -from .capirca_models import ( - CapircaPolicy, -) - __all__ = ( "AddressObject", "AddressObjectGroup", "AddressObjectGroupM2M", + "ApplicationObject", + "ApplicationObjectGroup", + "ApplicationObjectGroupM2M", "CapircaPolicy", "DestAddrGroupM2M", "DestAddrM2M", diff --git a/nautobot_firewall_models/models/address.py b/nautobot_firewall_models/models/address.py new file mode 100644 index 00000000..6216e1b3 --- /dev/null +++ b/nautobot_firewall_models/models/address.py @@ -0,0 +1,281 @@ +"""Models for the Firewall plugin.""" +# pylint: disable=duplicate-code, too-many-lines + +from django.core.exceptions import ValidationError +from django.db import models +from django.db.models import Q +from django.db.models.constraints import UniqueConstraint +from django.urls import reverse +from nautobot.core.models.generics import BaseModel, PrimaryModel +from nautobot.extras.models import StatusField +from nautobot.extras.utils import extras_features +from nautobot.ipam.fields import VarbinaryIPField +from netaddr import IPAddress + +from nautobot_firewall_models.utils import get_default_status + + +########################### +# Core Models +########################### + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class IPRange(PrimaryModel): + """IPRange model to track ranges of IPs in firewall rules.""" + + start_address = VarbinaryIPField( + null=False, + db_index=True, + help_text="Starting IPv4 or IPv6 host address", + ) + end_address = VarbinaryIPField( + null=False, + db_index=True, + help_text="Ending IPv4 or IPv6 host address", + ) + vrf = models.ForeignKey( + to="ipam.VRF", on_delete=models.PROTECT, related_name="ip_ranges", blank=True, null=True, verbose_name="VRF" + ) + description = models.CharField( + max_length=200, + blank=True, + ) + size = models.PositiveIntegerField(editable=False) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["start_address"] + verbose_name = "IP Range" + verbose_name_plural = "IP Ranges" + constraints = [ + UniqueConstraint(fields=["start_address", "end_address", "vrf"], name="unique_with_vrf"), + UniqueConstraint(fields=["start_address", "end_address"], condition=Q(vrf=None), name="unique_without_vrf"), + ] + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:iprange", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return f"{self.start_address}-{self.end_address}" + + def save(self, *args, **kwargs): + """Overloads to inject size attr.""" + # Record the range's size (number of IP addresses) + self.clean() + self.size = int(IPAddress(self.end_address) - IPAddress(self.start_address)) + 1 + super().save(*args, **kwargs) + + def clean(self, *args, **kwargs): + """Overloads to validate attr for form verification.""" + if not getattr(self, "start_address") or not getattr(self, "end_address"): + raise ValidationError("Must have `start_address` and `end_address`.") + if IPAddress(self.start_address) > IPAddress(self.end_address): + raise ValidationError("`end_address` must be >= than `start_address`.") + + super().clean(*args, **kwargs) + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class FQDN(PrimaryModel): + """Models fully qualified domain names, can be used on some firewall in place of a static IP.""" + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField( + max_length=254, unique=True, help_text="Resolvable fully qualified domain name (e.g. networktocode.com)" + ) + ip_addresses = models.ManyToManyField( + to="ipam.IPAddress", + blank=True, + through="FQDNIPAddressM2M", + related_name="fqdns", + help_text="IP(s) an FQDN should resolve to.", + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name = "FQDN" + verbose_name_plural = "FQDNs" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:fqdn", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.name + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class AddressObject(PrimaryModel): + """Intermediate model to aggregate underlying address items, to allow for easier management.""" + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, unique=True, help_text="Name descriptor for an address object type.") + fqdn = models.ForeignKey(to="nautobot_firewall_models.FQDN", on_delete=models.PROTECT, null=True, blank=True) + ip_range = models.ForeignKey(to="nautobot_firewall_models.IPRange", on_delete=models.PROTECT, null=True, blank=True) + ip_address = models.ForeignKey(to="ipam.IPAddress", on_delete=models.PROTECT, null=True, blank=True) + prefix = models.ForeignKey(to="ipam.Prefix", on_delete=models.PROTECT, null=True, blank=True) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "Address Objects" + + def get_address_info(self): + """Method to Return the actual AddressObject type.""" + keys = ["ip_range", "fqdn", "prefix", "ip_address"] + for key in keys: + if getattr(self, key): + return (key, getattr(self, key)) + return (None, None) + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:addressobject", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.name + + def save(self, *args, **kwargs): + """Overloads to enforce clear.""" + self.clean() + super().save(*args, **kwargs) + + def clean(self, *args, **kwargs): + """Overloads to validate attr for form verification.""" + address_types = ["fqdn", "ip_range", "ip_address", "prefix"] + address_count = 0 + for i in address_types: + if hasattr(self, i) and getattr(self, i) is not None: + address_count += 1 + if address_count != 1: + raise ValidationError(f"Must specify only one address from type {address_types}, {address_count} found.") + + super().clean(*args, **kwargs) + + @property + def address(self): # pylint: disable=inconsistent-return-statements + """Returns the assigned address object.""" + for i in ["fqdn", "ip_range", "ip_address", "prefix"]: + if getattr(self, i): + return getattr(self, i) + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class AddressObjectGroup(PrimaryModel): + """Groups together AddressObjects to better mimic grouping sets of address objects that have a some commonality.""" + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, unique=True, help_text="Name descriptor for a group address objects.") + address_objects = models.ManyToManyField( + to="nautobot_firewall_models.AddressObject", + blank=True, + through="AddressObjectGroupM2M", + related_name="address_object_groups", + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "Address Object Groups" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:addressobjectgroup", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.name + + +########################### +# Through Models +########################### + + +class AddressObjectGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated AddressObject if assigned to a AddressObjectGroup.""" + + address = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) + address_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.CASCADE) + + +class FQDNIPAddressM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated IPAddress if assigned to a FQDN.""" + + fqdn = models.ForeignKey("nautobot_firewall_models.FQDN", on_delete=models.CASCADE) + ip_address = models.ForeignKey("ipam.IPAddress", on_delete=models.PROTECT) diff --git a/nautobot_firewall_models/models/capirca_models.py b/nautobot_firewall_models/models/capirca_policy.py similarity index 100% rename from nautobot_firewall_models/models/capirca_models.py rename to nautobot_firewall_models/models/capirca_policy.py diff --git a/nautobot_firewall_models/models/core_models.py b/nautobot_firewall_models/models/core_models.py deleted file mode 100644 index dde913d6..00000000 --- a/nautobot_firewall_models/models/core_models.py +++ /dev/null @@ -1,918 +0,0 @@ -"""Models for the Firewall plugin.""" -# pylint: disable=duplicate-code - -from django.core.exceptions import ValidationError -from django.db import models -from django.db.models import Q -from django.db.models.constraints import UniqueConstraint -from django.urls import reverse -from nautobot.core.models.generics import PrimaryModel -from nautobot.extras.models import StatusField -from nautobot.extras.models.tags import TaggedItem -from nautobot.extras.utils import extras_features -from nautobot.ipam.fields import VarbinaryIPField -from netaddr import IPAddress -from taggit.managers import TaggableManager - -from nautobot_firewall_models import choices, validators -from nautobot_firewall_models.utils import get_default_status, model_to_json - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class IPRange(PrimaryModel): - """IPRange model to track ranges of IPs in firewall rules.""" - - start_address = VarbinaryIPField( - null=False, - db_index=True, - help_text="Starting IPv4 or IPv6 host address", - ) - end_address = VarbinaryIPField( - null=False, - db_index=True, - help_text="Ending IPv4 or IPv6 host address", - ) - vrf = models.ForeignKey( - to="ipam.VRF", on_delete=models.PROTECT, related_name="ip_ranges", blank=True, null=True, verbose_name="VRF" - ) - description = models.CharField( - max_length=200, - blank=True, - ) - size = models.PositiveIntegerField(editable=False) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["start_address"] - verbose_name_plural = "IP Ranges" - constraints = [ - UniqueConstraint(fields=["start_address", "end_address", "vrf"], name="unique_with_vrf"), - UniqueConstraint(fields=["start_address", "end_address"], condition=Q(vrf=None), name="unique_without_vrf"), - ] - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:iprange", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - return f"{self.start_address}-{self.end_address}" - - def save(self, *args, **kwargs): - """Overloads to inject size attr.""" - # Record the range's size (number of IP addresses) - self.clean() - self.size = int(IPAddress(self.end_address) - IPAddress(self.start_address)) + 1 - super().save(*args, **kwargs) - - def clean(self, *args, **kwargs): - """Overloads to validate attr for form verification.""" - if not getattr(self, "start_address") or not getattr(self, "end_address"): - raise ValidationError("Must have `start_address` and `end_address`.") - if IPAddress(self.start_address) > IPAddress(self.end_address): - raise ValidationError("`end_address` must be >= than `start_address`.") - - super().clean(*args, **kwargs) - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class FQDN(PrimaryModel): - """Models fully qualified domain names, can be used on some firewall in place of a static IP.""" - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField( - max_length=254, unique=True, help_text="Resolvable fully qualified domain name (e.g. networktocode.com)" - ) - ip_addresses = models.ManyToManyField( - to="ipam.IPAddress", - blank=True, - through="FQDNIPAddressM2M", - related_name="fqdns", - help_text="IP(s) an FQDN should resolve to.", - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name = "FQDN" - verbose_name_plural = "FQDNs" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:fqdn", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - return self.name - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class AddressObject(PrimaryModel): - """Intermediate model to aggregate underlying address items, to allow for easier management.""" - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField(max_length=100, unique=True, help_text="Name descriptor for an address object type.") - fqdn = models.ForeignKey(to=FQDN, on_delete=models.PROTECT, null=True, blank=True) - ip_range = models.ForeignKey(to=IPRange, on_delete=models.PROTECT, null=True, blank=True) - ip_address = models.ForeignKey(to="ipam.IPAddress", on_delete=models.PROTECT, null=True, blank=True) - prefix = models.ForeignKey(to="ipam.Prefix", on_delete=models.PROTECT, null=True, blank=True) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name_plural = "Address Objects" - - def get_address_info(self): - """Method to Return the actual AddressObject type.""" - keys = ["ip_range", "fqdn", "prefix", "ip_address"] - for key in keys: - if getattr(self, key): - return (key, getattr(self, key)) - return (None, None) - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:addressobject", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - return self.name - - def save(self, *args, **kwargs): - """Overloads to enforce clear.""" - self.clean() - super().save(*args, **kwargs) - - def clean(self, *args, **kwargs): - """Overloads to validate attr for form verification.""" - address_types = ["fqdn", "ip_range", "ip_address", "prefix"] - address_count = 0 - for i in address_types: - if hasattr(self, i) and getattr(self, i) is not None: - address_count += 1 - if address_count != 1: - raise ValidationError(f"Must specify only one address from type {address_types}, {address_count} found.") - - super().clean(*args, **kwargs) - - @property - def address(self): # pylint: disable=inconsistent-return-statements - """Returns the assigned address object.""" - for i in ["fqdn", "ip_range", "ip_address", "prefix"]: - if getattr(self, i): - return getattr(self, i) - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class AddressObjectGroup(PrimaryModel): - """Groups together AddressObjects to better mimic grouping sets of address objects that have a some commonality.""" - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField(max_length=100, unique=True, help_text="Name descriptor for a group address objects.") - address_objects = models.ManyToManyField( - to=AddressObject, blank=True, through="AddressObjectGroupM2M", related_name="address_object_groups" - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name_plural = "Address Object Groups" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:addressobjectgroup", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - return self.name - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class UserObject(PrimaryModel): - """Source users can be used to identify the origin of traffic for a user on some firewalls.""" - - username = models.CharField( - max_length=100, unique=True, help_text="Signifies the username in identify provider (e.g. john.smith)" - ) - name = models.CharField( - max_length=100, - blank=True, - help_text="Signifies the name of the user, commonly first & last name (e.g. John Smith)", - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["username"] - verbose_name_plural = "User Objects" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:userobject", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - return self.username - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class UserObjectGroup(PrimaryModel): - """Grouping of individual user objects, does NOT have any relationship to AD groups or any other IDP group.""" - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField(max_length=100, unique=True) - user_objects = models.ManyToManyField( - to=UserObject, blank=True, through="UserObjectGroupM2M", related_name="user_object_groups" - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name_plural = "User Object Groups" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:userobjectgroup", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - return self.name - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class Zone(PrimaryModel): - """Zones common on firewalls and are typically seen as representations of area (e.g. DMZ trust untrust).""" - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField(max_length=100, unique=True, help_text="Name of the zone (e.g. trust)") - vrfs = models.ManyToManyField(to="ipam.VRF", blank=True, through="ZoneVRFM2M", related_name="zones") - interfaces = models.ManyToManyField( - to="dcim.Interface", blank=True, through="ZoneInterfaceM2M", related_name="zones" - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name_plural = "Zones" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:zone", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - return self.name - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class ServiceObject(PrimaryModel): - """ServiceObject matches a IANA IP Protocol with a name and optional port number (e.g. TCP HTTPS 443).""" - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField(max_length=100, help_text="Name of the service (e.g. HTTP)") - port = models.CharField( - null=True, - blank=True, - validators=[validators.validate_port], - max_length=20, - help_text="The port or port range to tie to a service (e.g. HTTP would be port 80)", - ) - ip_protocol = models.CharField( - choices=choices.IP_PROTOCOL_CHOICES, max_length=20, help_text="IANA IP Protocol (e.g. TCP UDP ICMP)" - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name_plural = "Service Objects" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:serviceobject", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - if self.port: - return f"{self.name} ({self.ip_protocol}/{self.port})" - return f"{self.name} ({self.ip_protocol})" - - def save(self, *args, **kwargs): - """Overload save to call full_clean to ensure validators run.""" - self.full_clean() - super().save(*args, **kwargs) - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class ServiceObjectGroup(PrimaryModel): - """Groups service objects.""" - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField(max_length=100, unique=True) - service_objects = models.ManyToManyField( - to=ServiceObject, blank=True, through="ServiceObjectGroupM2M", related_name="service_object_groups" - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name_plural = "Service Object Groups" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:serviceobjectgroup", args=[self.pk]) - - def __str__(self): - """Stringify instance.""" - return self.name - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class PolicyRule(PrimaryModel): - """ - A PolicyRule is a the equivalent of a single in a firewall policy or access list. - - Firewall policies are typically made up of several individual rules. - """ - - name = models.CharField(max_length=100) - tags = TaggableManager(through=TaggedItem) - 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_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" - ) - 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_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" - ) - 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( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - 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) - - clone_fields = [ - "source_users", - "source_user_groups", - "source_addresses", - "source_address_groups", - "source_zone", - "source_services", - "source_service_groups", - "destination_addresses", - "destination_address_groups", - "destination_zone", - "destination_services", - "destination_service_groups", - "action", - "log", - "status", - ] - - class Meta: - """Meta class.""" - - ordering = ["index"] - verbose_name_plural = "Policy Rules" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:policyrule", args=[self.pk]) - - def rule_details(self): - """Convience method to convert to more consumable dictionary.""" - row = {} - row["rule"] = self - 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_groups"] = self.destination_address_groups.all() - row["destination_addresses"] = self.destination_addresses.all() - row["destination_zone"] = self.destination_zone - row["destination_services"] = self.destination_services.all() - row["destination_service_groups"] = self.destination_service_groups.all() - - row["action"] = self.action - row["log"] = self.log - row["status"] = self.status - row["request_id"] = self.request_id - return row - - def to_json(self): - """Convience method to convert to json.""" - return model_to_json(self) - - def __str__(self): - """Stringify instance.""" - if self.request_id and self.name: - return f"{self.name} - {self.request_id}" - if self.name: - return self.name - return str(self.id) - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class Policy(PrimaryModel): - """ - The overarching model that is the full firewall policy with all underlying rules and child objects. - - Each Policy can be assigned to both devices and to dynamic groups which in turn can assign the policy to a related device. - """ - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField(max_length=100, unique=True) - policy_rules = models.ManyToManyField(to=PolicyRule, through="PolicyRuleM2M", related_name="policies") - assigned_devices = models.ManyToManyField( - to="dcim.Device", through="PolicyDeviceM2M", related_name="firewall_policies" - ) - assigned_dynamic_groups = models.ManyToManyField( - to="extras.DynamicGroup", through="PolicyDynamicGroupM2M", related_name="firewall_policies" - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - tenant = models.ForeignKey( - to="tenancy.Tenant", - on_delete=models.PROTECT, - related_name="policies", - blank=True, - null=True, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name_plural = "Policies" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:policy", args=[self.pk]) - - def policy_details(self): - """Convience method to convert to a Python list of dictionaries.""" - data = [] - for policy_rule in self.policy_rules.all(): - data.append(policy_rule.rule_details()) - return data - - def to_json(self): - """Convience method to convert to json.""" - return model_to_json(self, "nautobot_firewall_models.api.serializers.PolicyDeepSerializer") - - def __str__(self): - """Stringify instance.""" - return self.name - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class NATPolicyRule(PrimaryModel): - """ - A NATPolicyRule is the equivalent of a single rule in a NAT policy or access list. - - NAT policies are typically made up of several individual rules. - """ - - # Metadata - name = models.CharField(max_length=100) - tags = TaggableManager(through=TaggedItem) - remark = models.BooleanField(default=False) - log = models.BooleanField(default=False) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - 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) - - # Data that can not undergo a translation - source_zone = models.ForeignKey( - to=Zone, null=True, blank=True, on_delete=models.SET_NULL, related_name="source_nat_policy_rules" - ) - destination_zone = models.ForeignKey( - to=Zone, on_delete=models.SET_NULL, null=True, blank=True, related_name="destination_nat_policy_rules" - ) - - # Original source data - original_source_addresses = models.ManyToManyField( - to=AddressObject, through="NATOrigSrcAddrM2M", related_name="original_source_nat_policy_rules" - ) - original_source_address_groups = models.ManyToManyField( - to=AddressObjectGroup, through="NATOrigSrcAddrGroupM2M", related_name="original_source_nat_policy_rules" - ) - original_source_services = models.ManyToManyField( - to=ServiceObject, through="NATOrigSrcSvcM2M", related_name="original_source_nat_policy_rules" - ) - original_source_service_groups = models.ManyToManyField( - to=ServiceObjectGroup, through="NATOrigSrcSvcGroupM2M", related_name="original_source_nat_policy_rules" - ) - - # Translated source data - translated_source_addresses = models.ManyToManyField( - to=AddressObject, through="NATTransSrcAddrM2M", related_name="translated_source_nat_policy_rules" - ) - translated_source_address_groups = models.ManyToManyField( - to=AddressObjectGroup, through="NATTransSrcAddrGroupM2M", related_name="translated_source_nat_policy_rules" - ) - translated_source_services = models.ManyToManyField( - to=ServiceObject, through="NATTransSrcSvcM2M", related_name="translated_source_nat_policy_rules" - ) - translated_source_service_groups = models.ManyToManyField( - to=ServiceObjectGroup, through="NATTransSrcSvcGroupM2M", related_name="translated_source_nat_policy_rules" - ) - - # Original destination data - original_destination_addresses = models.ManyToManyField( - to=AddressObject, through="NATOrigDestAddrM2M", related_name="original_destination_nat_policy_rules" - ) - original_destination_address_groups = models.ManyToManyField( - to=AddressObjectGroup, through="NATOrigDestAddrGroupM2M", related_name="original_destination_nat_policy_rules" - ) - original_destination_services = models.ManyToManyField( - to=ServiceObject, through="NATOrigDestSvcM2M", related_name="original_destination_nat_policy_rules" - ) - original_destination_service_groups = models.ManyToManyField( - to=ServiceObjectGroup, through="NATOrigDestSvcGroupM2M", related_name="original_destination_nat_policy_rules" - ) - - # Translated destination data - translated_destination_addresses = models.ManyToManyField( - to=AddressObject, through="NATTransDestAddrM2M", related_name="translated_destination_nat_policy_rules" - ) - translated_destination_address_groups = models.ManyToManyField( - to=AddressObjectGroup, - through="NATTransDestAddrGroupM2M", - related_name="translated_destination_nat_policy_rules", - ) - translated_destination_services = models.ManyToManyField( - to=ServiceObject, through="NATTransDestSvcM2M", related_name="translated_destination_nat_policy_rules" - ) - translated_destination_service_groups = models.ManyToManyField( - to=ServiceObjectGroup, through="NATTransDestSvcGroupM2M", related_name="translated_destination_nat_policy_rules" - ) - - clone_fields = [ - "destination_zone", - "source_zone", - "original_source_addresses", - "original_source_address_groups", - "original_source_services", - "original_source_service_groups", - "original_destination_addresses", - "original_destination_address_groups", - "original_destination_services", - "original_destination_service_groups", - "translated_source_addresses", - "translated_source_address_groups", - "translated_source_services", - "translated_source_service_groups", - "translated_destination_addresses", - "translated_destination_address_groups", - "translated_destination_services", - "translated_destination_service_groups", - "remark", - "log", - "status", - ] - - class Meta: - """Meta class.""" - - ordering = ["index"] - verbose_name_plural = "NAT Policy Rules" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:natpolicyrule", args=[self.pk]) - - def rule_details(self): - """Convenience method to convert to more consumable dictionary.""" - row = {} - row["rule"] = self - row["source_zone"] = self.source_zone - row["destination_zone"] = self.destination_zone - - row["original_source_address_groups"] = self.original_source_address_groups.all() - row["original_source_addresses"] = self.original_source_addresses.all() - row["original_source_services"] = self.original_source_services.all() - row["original_source_service_groups"] = self.original_source_service_groups.all() - - row["translated_source_address_groups"] = self.translated_source_address_groups.all() - row["translated_source_addresses"] = self.translated_source_addresses.all() - row["translated_source_services"] = self.translated_source_services.all() - row["translated_source_service_groups"] = self.translated_source_service_groups.all() - - row["original_destination_address_groups"] = self.original_destination_address_groups.all() - row["original_destination_addresses"] = self.original_destination_addresses.all() - row["original_destination_services"] = self.original_destination_services.all() - row["original_destination_service_groups"] = self.original_destination_service_groups.all() - - row["translated_destination_address_groups"] = self.translated_destination_address_groups.all() - row["translated_destination_addresses"] = self.translated_destination_addresses.all() - row["translated_destination_services"] = self.translated_destination_services.all() - row["translated_destination_service_groups"] = self.translated_destination_service_groups.all() - - row["remark"] = self.remark - row["log"] = self.log - row["status"] = self.status - row["request_id"] = self.request_id - return row - - def to_json(self): - """Convenience method to convert to json.""" - return model_to_json(self) - - def __str__(self): - """Stringify instance.""" - if self.request_id and self.name: - return f"{self.name} - {self.request_id}" - if self.name: - return self.name - return str(self.id) - - -@extras_features( - "custom_fields", - "custom_links", - "custom_validators", - "export_templates", - "graphql", - "relationships", - "statuses", - "webhooks", -) -class NATPolicy(PrimaryModel): - """ - The overarching model that is the full NAT policy with all underlying rules and child objects. - - Each NATPolicy can be assigned to both devices and to dynamic groups which in turn can assign the policy to a related device. - """ - - description = models.CharField( - max_length=200, - blank=True, - ) - name = models.CharField(max_length=100, unique=True) - nat_policy_rules = models.ManyToManyField(to=NATPolicyRule, through="NATPolicyRuleM2M", related_name="nat_policies") - assigned_devices = models.ManyToManyField( - to="dcim.Device", through="NATPolicyDeviceM2M", related_name="nat_policies" - ) - assigned_dynamic_groups = models.ManyToManyField( - to="extras.DynamicGroup", through="NATPolicyDynamicGroupM2M", related_name="nat_policies" - ) - status = StatusField( - on_delete=models.PROTECT, - related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related - default=get_default_status, - ) - tenant = models.ForeignKey( - to="tenancy.Tenant", - on_delete=models.PROTECT, - related_name="nat_policies", - blank=True, - null=True, - ) - - class Meta: - """Meta class.""" - - ordering = ["name"] - verbose_name_plural = "NAT Policies" - - def get_absolute_url(self): - """Return detail view URL.""" - return reverse("plugins:nautobot_firewall_models:natpolicy", args=[self.pk]) - - def policy_details(self): - """Convenience method to convert to a Python list of dictionaries.""" - return [rule.rule_details() for rule in self.nat_policy_rules.all()] - - def to_json(self): - """Convenience method to convert to json.""" - return model_to_json(self) - - def __str__(self): - """Stringify instance.""" - return self.name diff --git a/nautobot_firewall_models/models/nat_policy.py b/nautobot_firewall_models/models/nat_policy.py new file mode 100644 index 00000000..74da06dc --- /dev/null +++ b/nautobot_firewall_models/models/nat_policy.py @@ -0,0 +1,472 @@ +"""Models for the Firewall plugin.""" +# pylint: disable=duplicate-code, too-many-lines + +from django.db import models +from django.urls import reverse +from nautobot.core.models.generics import BaseModel, PrimaryModel +from nautobot.extras.models import StatusField +from nautobot.extras.models.tags import TaggedItem +from nautobot.extras.utils import extras_features +from taggit.managers import TaggableManager + +from nautobot_firewall_models.utils import get_default_status, model_to_json + + +########################### +# Core Models +########################### + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class NATPolicyRule(PrimaryModel): + """ + A NATPolicyRule is the equivalent of a single rule in a NAT policy or access list. + + NAT policies are typically made up of several individual rules. + """ + + # Metadata + name = models.CharField(max_length=100) + tags = TaggableManager(through=TaggedItem) + remark = models.BooleanField(default=False) + log = models.BooleanField(default=False) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + 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) + + # Data that can not undergo a translation + source_zone = models.ForeignKey( + to="nautobot_firewall_models.Zone", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="source_nat_policy_rules", + ) + destination_zone = models.ForeignKey( + to="nautobot_firewall_models.Zone", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="destination_nat_policy_rules", + ) + + # Original source data + original_source_addresses = models.ManyToManyField( + to="nautobot_firewall_models.AddressObject", + through="NATOrigSrcAddrM2M", + related_name="original_source_nat_policy_rules", + ) + original_source_address_groups = models.ManyToManyField( + to="nautobot_firewall_models.AddressObjectGroup", + through="NATOrigSrcAddrGroupM2M", + related_name="original_source_nat_policy_rules", + ) + original_source_services = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObject", + through="NATOrigSrcSvcM2M", + related_name="original_source_nat_policy_rules", + ) + original_source_service_groups = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObjectGroup", + through="NATOrigSrcSvcGroupM2M", + related_name="original_source_nat_policy_rules", + ) + + # Translated source data + translated_source_addresses = models.ManyToManyField( + to="nautobot_firewall_models.AddressObject", + through="NATTransSrcAddrM2M", + related_name="translated_source_nat_policy_rules", + ) + translated_source_address_groups = models.ManyToManyField( + to="nautobot_firewall_models.AddressObjectGroup", + through="NATTransSrcAddrGroupM2M", + related_name="translated_source_nat_policy_rules", + ) + translated_source_services = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObject", + through="NATTransSrcSvcM2M", + related_name="translated_source_nat_policy_rules", + ) + translated_source_service_groups = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObjectGroup", + through="NATTransSrcSvcGroupM2M", + related_name="translated_source_nat_policy_rules", + ) + + # Original destination data + original_destination_addresses = models.ManyToManyField( + to="nautobot_firewall_models.AddressObject", + through="NATOrigDestAddrM2M", + related_name="original_destination_nat_policy_rules", + ) + original_destination_address_groups = models.ManyToManyField( + to="nautobot_firewall_models.AddressObjectGroup", + through="NATOrigDestAddrGroupM2M", + related_name="original_destination_nat_policy_rules", + ) + original_destination_services = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObject", + through="NATOrigDestSvcM2M", + related_name="original_destination_nat_policy_rules", + ) + original_destination_service_groups = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObjectGroup", + through="NATOrigDestSvcGroupM2M", + related_name="original_destination_nat_policy_rules", + ) + + # Translated destination data + translated_destination_addresses = models.ManyToManyField( + to="nautobot_firewall_models.AddressObject", + through="NATTransDestAddrM2M", + related_name="translated_destination_nat_policy_rules", + ) + translated_destination_address_groups = models.ManyToManyField( + to="nautobot_firewall_models.AddressObjectGroup", + through="NATTransDestAddrGroupM2M", + related_name="translated_destination_nat_policy_rules", + ) + translated_destination_services = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObject", + through="NATTransDestSvcM2M", + related_name="translated_destination_nat_policy_rules", + ) + translated_destination_service_groups = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObjectGroup", + through="NATTransDestSvcGroupM2M", + related_name="translated_destination_nat_policy_rules", + ) + + clone_fields = [ + "destination_zone", + "source_zone", + "original_source_addresses", + "original_source_address_groups", + "original_source_services", + "original_source_service_groups", + "original_destination_addresses", + "original_destination_address_groups", + "original_destination_services", + "original_destination_service_groups", + "translated_source_addresses", + "translated_source_address_groups", + "translated_source_services", + "translated_source_service_groups", + "translated_destination_addresses", + "translated_destination_address_groups", + "translated_destination_services", + "translated_destination_service_groups", + "remark", + "log", + "status", + ] + + class Meta: + """Meta class.""" + + ordering = ["index"] + verbose_name = "NAT Policy Rule" + verbose_name_plural = "NAT Policy Rules" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:natpolicyrule", args=[self.pk]) + + def rule_details(self): + """Convenience method to convert to more consumable dictionary.""" + row = {} + row["rule"] = self + row["source_zone"] = self.source_zone + row["destination_zone"] = self.destination_zone + + row["original_source_address_groups"] = self.original_source_address_groups.all() + row["original_source_addresses"] = self.original_source_addresses.all() + row["original_source_services"] = self.original_source_services.all() + row["original_source_service_groups"] = self.original_source_service_groups.all() + + row["translated_source_address_groups"] = self.translated_source_address_groups.all() + row["translated_source_addresses"] = self.translated_source_addresses.all() + row["translated_source_services"] = self.translated_source_services.all() + row["translated_source_service_groups"] = self.translated_source_service_groups.all() + + row["original_destination_address_groups"] = self.original_destination_address_groups.all() + row["original_destination_addresses"] = self.original_destination_addresses.all() + row["original_destination_services"] = self.original_destination_services.all() + row["original_destination_service_groups"] = self.original_destination_service_groups.all() + + row["translated_destination_address_groups"] = self.translated_destination_address_groups.all() + row["translated_destination_addresses"] = self.translated_destination_addresses.all() + row["translated_destination_services"] = self.translated_destination_services.all() + row["translated_destination_service_groups"] = self.translated_destination_service_groups.all() + + row["remark"] = self.remark + row["log"] = self.log + row["status"] = self.status + row["request_id"] = self.request_id + return row + + def to_json(self): + """Convenience method to convert to json.""" + return model_to_json(self) + + def __str__(self): + """Stringify instance.""" + if self.request_id and self.name: + return f"{self.name} - {self.request_id}" + if self.name: + return self.name + return str(self.id) + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class NATPolicy(PrimaryModel): + """ + The overarching model that is the full NAT policy with all underlying rules and child objects. + + Each NATPolicy can be assigned to both devices and to dynamic groups which in turn can assign the policy to a related device. + """ + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, unique=True) + nat_policy_rules = models.ManyToManyField(to=NATPolicyRule, through="NATPolicyRuleM2M", related_name="nat_policies") + assigned_devices = models.ManyToManyField( + to="dcim.Device", through="NATPolicyDeviceM2M", related_name="nat_policies" + ) + assigned_dynamic_groups = models.ManyToManyField( + to="extras.DynamicGroup", through="NATPolicyDynamicGroupM2M", related_name="nat_policies" + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + tenant = models.ForeignKey( + to="tenancy.Tenant", + on_delete=models.PROTECT, + related_name="nat_policies", + blank=True, + null=True, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name = "NAT Policy" + verbose_name_plural = "NAT Policies" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:natpolicy", args=[self.pk]) + + def policy_details(self): + """Convenience method to convert to a Python list of dictionaries.""" + return [rule.rule_details() for rule in self.nat_policy_rules.all()] + + def to_json(self): + """Convenience method to convert to json.""" + return model_to_json(self) + + def __str__(self): + """Stringify instance.""" + return self.name + + +########################### +# Through Models +########################### + + +class NATOrigDestAddrGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated original destination AddressObject if assigned to a NATPolicyRule.""" + + addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATOrigDestAddrM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated original destination AddressObjectGroup if assigned to a NATPolicyRule.""" + + user = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATOrigDestSvcGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated original destination ServiceObjectGroup if assigned to a NATPolicyRule.""" + + svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATOrigDestSvcM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated original destination ServiceObject if assigned to a NATPolicyRule.""" + + svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATOrigSrcAddrGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated original source AddressObjectGroup if assigned to a NATPolicyRule.""" + + addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATOrigSrcAddrM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated original source AddressObject if assigned to a NATPolicyRule.""" + + addr = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATOrigSrcSvcGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated original source ServiceObjectGroup if assigned to a NATPolicyRule.""" + + svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATOrigSrcSvcM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated original source ServiceObject if assigned to a NATPolicyRule.""" + + svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATPolicyDeviceM2M(BaseModel): + """Through model to add weight to the NATPolicy & Device relationship.""" + + nat_policy = models.ForeignKey("nautobot_firewall_models.NATPolicy", on_delete=models.CASCADE) + device = models.ForeignKey("dcim.Device", on_delete=models.PROTECT) + weight = models.PositiveSmallIntegerField(default=100) + + class Meta: + """Meta class.""" + + ordering = ["weight"] + unique_together = ["nat_policy", "device"] + + +class NATPolicyDynamicGroupM2M(BaseModel): + """Through model to add weight to the NATPolicy & DynamicGroup relationship.""" + + nat_policy = models.ForeignKey("nautobot_firewall_models.NATPolicy", on_delete=models.CASCADE) + dynamic_group = models.ForeignKey("extras.DynamicGroup", on_delete=models.PROTECT) + weight = models.PositiveSmallIntegerField(default=100) + + class Meta: + """Meta class.""" + + ordering = ["weight"] + unique_together = ["nat_policy", "dynamic_group"] + + +class NATPolicyNATRuleM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated NATPolicyRule if assigned to a NATPolicy.""" + + nat_policy = models.ForeignKey("nautobot_firewall_models.NATPolicy", on_delete=models.CASCADE) + nat_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.PROTECT) + + +class NATPolicyRuleM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated NATPolicyRule if assigned to a NATPolicy.""" + + nat_policy = models.ForeignKey("nautobot_firewall_models.NATPolicy", on_delete=models.CASCADE) + nat_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.PROTECT) + + +class NATSrcUserGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated UserGroup if assigned to a NATPolicyRule.""" + + user_group = models.ForeignKey("nautobot_firewall_models.UserObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATSrcUserM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated User if assigned to a NATPolicyRule.""" + + user = models.ForeignKey("nautobot_firewall_models.UserObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATTransDestAddrM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated destination AddressObjectGroup if assigned to a NATPolicyRule.""" + + user = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATTransDestAddrGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated destination AddressObject if assigned to a NATPolicyRule.""" + + addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATTransDestSvcGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated destination ServiceObjectGroup if assigned to a NATPolicyRule.""" + + svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATTransDestSvcM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated destination ServiceObject if assigned to a NATPolicyRule.""" + + svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATTransSrcAddrGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated source AddressObjectGroup if assigned to a NATPolicyRule.""" + + addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATTransSrcAddrM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated source AddressObject if assigned to a NATPolicyRule.""" + + addr = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATTransSrcSvcGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated source ServiceObjectGroup if assigned to a NATPolicyRule.""" + + svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) + + +class NATTransSrcSvcM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated source ServiceObject if assigned to a NATPolicyRule.""" + + svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) + nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) diff --git a/nautobot_firewall_models/models/security_policy.py b/nautobot_firewall_models/models/security_policy.py new file mode 100644 index 00000000..61806b8d --- /dev/null +++ b/nautobot_firewall_models/models/security_policy.py @@ -0,0 +1,364 @@ +"""Models for the Firewall plugin.""" +# pylint: disable=duplicate-code, too-many-lines + +from django.db import models +from django.urls import reverse +from nautobot.core.models.generics import BaseModel, PrimaryModel +from nautobot.extras.models import StatusField +from nautobot.extras.models.tags import TaggedItem +from nautobot.extras.utils import extras_features +from taggit.managers import TaggableManager + +from nautobot_firewall_models import choices +from nautobot_firewall_models.utils import get_default_status, model_to_json + + +########################### +# Core Models +########################### + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class PolicyRule(PrimaryModel): + """ + A PolicyRule is a the equivalent of a single in a firewall policy or access list. + + Firewall policies are typically made up of several individual rules. + """ + + name = models.CharField(max_length=100) + tags = TaggableManager(through=TaggedItem) + source_users = models.ManyToManyField( + to="nautobot_firewall_models.UserObject", through="SrcUserM2M", related_name="policy_rules" + ) + source_user_groups = models.ManyToManyField( + to="nautobot_firewall_models.UserObjectGroup", through="SrcUserGroupM2M", related_name="policy_rules" + ) + source_addresses = models.ManyToManyField( + to="nautobot_firewall_models.AddressObject", through="SrcAddrM2M", related_name="source_policy_rules" + ) + source_address_groups = models.ManyToManyField( + to="nautobot_firewall_models.AddressObjectGroup", through="SrcAddrGroupM2M", related_name="source_policy_rules" + ) + source_zone = models.ForeignKey( + to="nautobot_firewall_models.Zone", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="source_policy_rules", + ) + source_services = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObject", through="SrcSvcM2M", related_name="source_policy_rules" + ) + source_service_groups = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObjectGroup", through="SrcSvcGroupM2M", related_name="source_policy_rules" + ) + destination_addresses = models.ManyToManyField( + to="nautobot_firewall_models.AddressObject", through="DestAddrM2M", related_name="destination_policy_rules" + ) + destination_address_groups = models.ManyToManyField( + to="nautobot_firewall_models.AddressObjectGroup", + through="DestAddrGroupM2M", + related_name="destination_policy_rules", + ) + destination_zone = models.ForeignKey( + to="nautobot_firewall_models.Zone", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="destination_policy_rules", + ) + destination_services = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObject", through="DestSvcM2M", related_name="destination_policy_rules" + ) + destination_service_groups = models.ManyToManyField( + to="nautobot_firewall_models.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( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + applications = models.ManyToManyField( + to="nautobot_firewall_models.ApplicationObject", through="ApplicationM2M", related_name="policy_rules" + ) + application_groups = models.ManyToManyField( + to="nautobot_firewall_models.ApplicationObjectGroup", through="ApplicationGroupM2M", related_name="policy_rules" + ) + 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) + + clone_fields = [ + "source_users", + "source_user_groups", + "source_addresses", + "source_address_groups", + "source_zone", + "source_services", + "source_service_groups", + "destination_addresses", + "destination_address_groups", + "destination_zone", + "destination_services", + "destination_service_groups", + "action", + "log", + "status", + ] + + class Meta: + """Meta class.""" + + ordering = ["index"] + verbose_name_plural = "Policy Rules" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:policyrule", args=[self.pk]) + + def rule_details(self): + """Convience method to convert to more consumable dictionary.""" + row = {} + row["rule"] = self + 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_groups"] = self.destination_address_groups.all() + row["destination_addresses"] = self.destination_addresses.all() + row["destination_zone"] = self.destination_zone + row["destination_services"] = self.destination_services.all() + row["destination_service_groups"] = self.destination_service_groups.all() + + row["action"] = self.action + row["log"] = self.log + row["status"] = self.status + row["request_id"] = self.request_id + return row + + def to_json(self): + """Convience method to convert to json.""" + return model_to_json(self) + + def __str__(self): + """Stringify instance.""" + if self.request_id and self.name: + return f"{self.name} - {self.request_id}" + if self.name: + return self.name + return str(self.id) + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class Policy(PrimaryModel): + """ + The overarching model that is the full firewall policy with all underlying rules and child objects. + + Each Policy can be assigned to both devices and to dynamic groups which in turn can assign the policy to a related device. + """ + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, unique=True) + policy_rules = models.ManyToManyField(to=PolicyRule, through="PolicyRuleM2M", related_name="policies") + assigned_devices = models.ManyToManyField( + to="dcim.Device", through="PolicyDeviceM2M", related_name="firewall_policies" + ) + assigned_dynamic_groups = models.ManyToManyField( + to="extras.DynamicGroup", through="PolicyDynamicGroupM2M", related_name="firewall_policies" + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + tenant = models.ForeignKey( + to="tenancy.Tenant", + on_delete=models.PROTECT, + related_name="policies", + blank=True, + null=True, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "Policies" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:policy", args=[self.pk]) + + def policy_details(self): + """Convience method to convert to a Python list of dictionaries.""" + data = [] + for policy_rule in self.policy_rules.all(): + data.append(policy_rule.rule_details()) + return data + + def to_json(self): + """Convience method to convert to json.""" + return model_to_json(self, "nautobot_firewall_models.api.serializers.PolicyDeepSerializer") + + def __str__(self): + """Stringify instance.""" + return self.name + + +########################### +# Through Models +########################### + + +class ApplicationM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated destination ApplicationObject if assigned to a PolicyRule.""" + + app = models.ForeignKey("nautobot_firewall_models.ApplicationObject", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class ApplicationGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated destination ApplicationObjectGroup if assigned to a PolicyRule.""" + + app_group = models.ForeignKey("nautobot_firewall_models.ApplicationObjectGroup", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class DestAddrGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated destination Address if assigned to a PolicyRule.""" + + addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class DestAddrM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated destination AddressGroup if assigned to a PolicyRule.""" + + user = models.ForeignKey("nautobot_firewall_models.AddressObject", 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 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) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class PolicyDeviceM2M(BaseModel): + """Through model to add weight to the the Policy & Device relationship.""" + + policy = models.ForeignKey("nautobot_firewall_models.Policy", on_delete=models.CASCADE) + device = models.ForeignKey("dcim.Device", on_delete=models.PROTECT) + weight = models.PositiveSmallIntegerField(default=100) + + class Meta: + """Meta class.""" + + ordering = ["weight"] + unique_together = ["policy", "device"] + + +class PolicyDynamicGroupM2M(BaseModel): + """Through model to add weight to the the Policy & DynamicGroup relationship.""" + + policy = models.ForeignKey("nautobot_firewall_models.Policy", on_delete=models.CASCADE) + dynamic_group = models.ForeignKey("extras.DynamicGroup", on_delete=models.PROTECT) + weight = models.PositiveSmallIntegerField(default=100) + + class Meta: + """Meta class.""" + + ordering = ["weight"] + unique_together = ["policy", "dynamic_group"] + + +class PolicyRuleM2M(BaseModel): + """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) + + class Meta: + """Meta class.""" + + ordering = ["rule__index"] + + +class SrcAddrM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated source Address if assigned to a PolicyRule.""" + + addr = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class SrcAddrGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated source AddressGroup if assigned to a PolicyRule.""" + + addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class SrcUserM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated User if assigned to a PolicyRule.""" + + user = models.ForeignKey("nautobot_firewall_models.UserObject", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +class SrcUserGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated UserGroup if assigned to a PolicyRule.""" + + user_group = models.ForeignKey("nautobot_firewall_models.UserObjectGroup", on_delete=models.PROTECT) + pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) + + +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) diff --git a/nautobot_firewall_models/models/service.py b/nautobot_firewall_models/models/service.py new file mode 100644 index 00000000..ea370580 --- /dev/null +++ b/nautobot_firewall_models/models/service.py @@ -0,0 +1,248 @@ +"""Models for the Firewall plugin.""" +# pylint: disable=duplicate-code, too-many-lines + +from django.db import models +from django.urls import reverse +from nautobot.core.models.generics import BaseModel, PrimaryModel +from nautobot.extras.models import StatusField +from nautobot.extras.utils import extras_features + +from nautobot_firewall_models import choices, validators +from nautobot_firewall_models.utils import get_default_status + + +########################### +# Core Models +########################### + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class ApplicationObject(PrimaryModel): + """Intermediate model to aggregate underlying application items, to allow for easier management.""" + + description = models.CharField( + max_length=200, + blank=True, + ) + category = models.CharField(max_length=48, blank=True, help_text="Category of application.") + subcategory = models.CharField(max_length=48, blank=True, help_text="Sub-category of application.") + technology = models.CharField(max_length=48, blank=True, help_text="Type of application technology.") + risk = models.PositiveIntegerField(blank=True, help_text="Assessed risk of the application.") + default_type = models.CharField(max_length=48, blank=True, help_text="Default type, i.e. port or app-id.") + name = models.CharField(max_length=100, unique=True, help_text="Name descriptor for an application object type.") + default_ip_protocol = models.CharField( + max_length=48, blank=True, help_text="Name descriptor for an application object type." + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "Application Objects" + + def get_application_info(self): + """Method to Return the actual ApplicationObject type.""" + keys = ["description", "category", "subcategory", "name"] + for key in keys: + if getattr(self, key): + return (key, getattr(self, key)) + return (None, None) + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:applicationobject", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.name + + def save(self, *args, **kwargs): + """Overloads to enforce clear.""" + self.clean() + super().save(*args, **kwargs) + + @property + def application(self): # pylint: disable=inconsistent-return-statements + """Returns the assigned application object.""" + for i in ["description", "category", "subcategory", "name"]: + if getattr(self, i): + return getattr(self, i) + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class ApplicationObjectGroup(PrimaryModel): + """Groups together ApplicationObjects to better mimic grouping sets of application objects that have a some commonality.""" + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, unique=True, help_text="Name descriptor for a group application objects.") + application_objects = models.ManyToManyField( + to="nautobot_firewall_models.ApplicationObject", + blank=True, + through="ApplicationObjectGroupM2M", + related_name="application_object_groups", + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "Application Object Groups" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:applicationobjectgroup", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.name + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class ServiceObject(PrimaryModel): + """ServiceObject matches a IANA IP Protocol with a name and optional port number (e.g. TCP HTTPS 443).""" + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, help_text="Name of the service (e.g. HTTP)") + port = models.CharField( + null=True, + blank=True, + validators=[validators.validate_port], + max_length=20, + help_text="The port or port range to tie to a service (e.g. HTTP would be port 80)", + ) + ip_protocol = models.CharField( + choices=choices.IP_PROTOCOL_CHOICES, max_length=20, help_text="IANA IP Protocol (e.g. TCP UDP ICMP)" + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "Service Objects" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:serviceobject", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + if self.port: + return f"{self.name} ({self.ip_protocol}/{self.port})" + return f"{self.name} ({self.ip_protocol})" + + def save(self, *args, **kwargs): + """Overload save to call full_clean to ensure validators run.""" + self.full_clean() + super().save(*args, **kwargs) + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class ServiceObjectGroup(PrimaryModel): + """Groups service objects.""" + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, unique=True) + service_objects = models.ManyToManyField( + to="nautobot_firewall_models.ServiceObject", + blank=True, + through="ServiceObjectGroupM2M", + related_name="service_object_groups", + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "Service Object Groups" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:serviceobjectgroup", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.name + + +########################### +# Through Models +########################### + + +class ApplicationObjectGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated ApplicationObject if assigned to a ApplicationObjectGroup.""" + + application = models.ForeignKey("nautobot_firewall_models.ApplicationObject", on_delete=models.PROTECT) + application_group = models.ForeignKey("nautobot_firewall_models.ApplicationObjectGroup", on_delete=models.CASCADE) + + +class ServiceObjectGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated ServiceGroup if assigned to a PolicyRule.""" + + service = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) + service_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.CASCADE) diff --git a/nautobot_firewall_models/models/through_models.py b/nautobot_firewall_models/models/through_models.py deleted file mode 100644 index f6534936..00000000 --- a/nautobot_firewall_models/models/through_models.py +++ /dev/null @@ -1,324 +0,0 @@ -"""Set of through intermediate models.""" - -from django.db import models -from nautobot.core.models.generics import BaseModel - - -class AddressObjectGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated AddressObject if assigned to a AddressObjectGroup.""" - - address = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) - address_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.CASCADE) - - -class DestAddrGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated destination Address if assigned to a PolicyRule.""" - - addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) - pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) - - -class DestAddrM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated destination AddressGroup if assigned to a PolicyRule.""" - - user = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) - pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) - - -class FQDNIPAddressM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated IPAddress if assigned to a FQDN.""" - - fqdn = models.ForeignKey("nautobot_firewall_models.FQDN", on_delete=models.CASCADE) - ip_address = models.ForeignKey("ipam.IPAddress", on_delete=models.PROTECT) - - -class PolicyDeviceM2M(BaseModel): - """Through model to add weight to the the Policy & Device relationship.""" - - policy = models.ForeignKey("nautobot_firewall_models.Policy", on_delete=models.CASCADE) - device = models.ForeignKey("dcim.Device", on_delete=models.PROTECT) - weight = models.PositiveSmallIntegerField(default=100) - - class Meta: - """Meta class.""" - - ordering = ["weight"] - unique_together = ["policy", "device"] - - -class PolicyDynamicGroupM2M(BaseModel): - """Through model to add weight to the the Policy & DynamicGroup relationship.""" - - policy = models.ForeignKey("nautobot_firewall_models.Policy", on_delete=models.CASCADE) - dynamic_group = models.ForeignKey("extras.DynamicGroup", on_delete=models.PROTECT) - weight = models.PositiveSmallIntegerField(default=100) - - class Meta: - """Meta class.""" - - ordering = ["weight"] - unique_together = ["policy", "dynamic_group"] - - -class PolicyRuleM2M(BaseModel): - """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) - - class Meta: - """Meta class.""" - - ordering = ["rule__index"] - - -class ServiceObjectGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated ServiceGroup if assigned to a PolicyRule.""" - - service = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) - service_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.CASCADE) - - -class SrcAddrM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated source Address if assigned to a PolicyRule.""" - - addr = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) - pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) - - -class SrcAddrGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated source AddressGroup if assigned to a PolicyRule.""" - - addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) - pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) - - -class SrcUserM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated User if assigned to a PolicyRule.""" - - user = models.ForeignKey("nautobot_firewall_models.UserObject", on_delete=models.PROTECT) - pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) - - -class SrcUserGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated UserGroup if assigned to a PolicyRule.""" - - user_group = models.ForeignKey("nautobot_firewall_models.UserObjectGroup", on_delete=models.PROTECT) - pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) - - -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 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) - pol_rule = models.ForeignKey("nautobot_firewall_models.PolicyRule", on_delete=models.CASCADE) - - -class UserObjectGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated User if assigned to a UserGroup.""" - - user = models.ForeignKey("nautobot_firewall_models.UserObject", on_delete=models.PROTECT) - user_group = models.ForeignKey("nautobot_firewall_models.UserObjectGroup", on_delete=models.CASCADE) - - -class ZoneInterfaceM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated Interface if assigned to a Zone.""" - - zone = models.ForeignKey("nautobot_firewall_models.Zone", on_delete=models.CASCADE) - interface = models.ForeignKey("dcim.Interface", on_delete=models.PROTECT) - - -class ZoneVRFM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated VRF if assigned to a Zone.""" - - zone = models.ForeignKey("nautobot_firewall_models.Zone", on_delete=models.CASCADE) - vrf = models.ForeignKey("ipam.vrf", on_delete=models.PROTECT) - - -class NATPolicyNATRuleM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated NATPolicyRule if assigned to a NATPolicy.""" - - nat_policy = models.ForeignKey("nautobot_firewall_models.NATPolicy", on_delete=models.CASCADE) - nat_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.PROTECT) - - -class NATPolicyRuleM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated NATPolicyRule if assigned to a NATPolicy.""" - - nat_policy = models.ForeignKey("nautobot_firewall_models.NATPolicy", on_delete=models.CASCADE) - nat_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.PROTECT) - - -class NATPolicyDeviceM2M(BaseModel): - """Through model to add weight to the NATPolicy & Device relationship.""" - - nat_policy = models.ForeignKey("nautobot_firewall_models.NATPolicy", on_delete=models.CASCADE) - device = models.ForeignKey("dcim.Device", on_delete=models.PROTECT) - weight = models.PositiveSmallIntegerField(default=100) - - class Meta: - """Meta class.""" - - ordering = ["weight"] - unique_together = ["nat_policy", "device"] - - -class NATPolicyDynamicGroupM2M(BaseModel): - """Through model to add weight to the NATPolicy & DynamicGroup relationship.""" - - nat_policy = models.ForeignKey("nautobot_firewall_models.NATPolicy", on_delete=models.CASCADE) - dynamic_group = models.ForeignKey("extras.DynamicGroup", on_delete=models.PROTECT) - weight = models.PositiveSmallIntegerField(default=100) - - class Meta: - """Meta class.""" - - ordering = ["weight"] - unique_together = ["nat_policy", "dynamic_group"] - - -class NATSrcUserM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated User if assigned to a NATPolicyRule.""" - - user = models.ForeignKey("nautobot_firewall_models.UserObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATSrcUserGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated UserGroup if assigned to a NATPolicyRule.""" - - user_group = models.ForeignKey("nautobot_firewall_models.UserObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATOrigSrcAddrM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated original source AddressObject if assigned to a NATPolicyRule.""" - - addr = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATOrigSrcAddrGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated original source AddressObjectGroup if assigned to a NATPolicyRule.""" - - addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATOrigSrcSvcM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated original source ServiceObject if assigned to a NATPolicyRule.""" - - svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATOrigSrcSvcGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated original source ServiceObjectGroup if assigned to a NATPolicyRule.""" - - svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATTransSrcAddrM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated source AddressObject if assigned to a NATPolicyRule.""" - - addr = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATTransSrcAddrGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated source AddressObjectGroup if assigned to a NATPolicyRule.""" - - addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATTransSrcSvcM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated source ServiceObject if assigned to a NATPolicyRule.""" - - svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATTransSrcSvcGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated source ServiceObjectGroup if assigned to a NATPolicyRule.""" - - svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATOrigDestAddrM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated original destination AddressObjectGroup if assigned to a NATPolicyRule.""" - - user = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATOrigDestAddrGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated original destination AddressObject if assigned to a NATPolicyRule.""" - - addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATOrigDestSvcM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated original destination ServiceObject if assigned to a NATPolicyRule.""" - - svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATOrigDestSvcGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated original destination ServiceObjectGroup if assigned to a NATPolicyRule.""" - - svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATTransDestAddrM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated destination AddressObjectGroup if assigned to a NATPolicyRule.""" - - user = models.ForeignKey("nautobot_firewall_models.AddressObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATTransDestAddrGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated destination AddressObject if assigned to a NATPolicyRule.""" - - addr_group = models.ForeignKey("nautobot_firewall_models.AddressObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATTransDestSvcM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated destination ServiceObject if assigned to a NATPolicyRule.""" - - svc = models.ForeignKey("nautobot_firewall_models.ServiceObject", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) - - -class NATTransDestSvcGroupM2M(BaseModel): - """Custom through model to on_delete=models.PROTECT to prevent deleting associated translated destination ServiceObjectGroup if assigned to a NATPolicyRule.""" - - svc_group = models.ForeignKey("nautobot_firewall_models.ServiceObjectGroup", on_delete=models.PROTECT) - nat_pol_rule = models.ForeignKey("nautobot_firewall_models.NATPolicyRule", on_delete=models.CASCADE) diff --git a/nautobot_firewall_models/models/user.py b/nautobot_firewall_models/models/user.py new file mode 100644 index 00000000..3c436e28 --- /dev/null +++ b/nautobot_firewall_models/models/user.py @@ -0,0 +1,114 @@ +"""Models for the Firewall plugin.""" +# pylint: disable=duplicate-code + +from django.db import models +from django.urls import reverse +from nautobot.core.models.generics import BaseModel, PrimaryModel +from nautobot.extras.models import StatusField +from nautobot.extras.utils import extras_features + +from nautobot_firewall_models.utils import get_default_status + + +########################### +# Core Models +########################### + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class UserObject(PrimaryModel): + """Source users can be used to identify the origin of traffic for a user on some firewalls.""" + + username = models.CharField( + max_length=100, unique=True, help_text="Signifies the username in identify provider (e.g. john.smith)" + ) + name = models.CharField( + max_length=100, + blank=True, + help_text="Signifies the name of the user, commonly first & last name (e.g. John Smith)", + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["username"] + verbose_name_plural = "User Objects" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:userobject", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.username + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class UserObjectGroup(PrimaryModel): + """Grouping of individual user objects, does NOT have any relationship to AD groups or any other IDP group.""" + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, unique=True) + user_objects = models.ManyToManyField( + to="nautobot_firewall_models.UserObject", + blank=True, + through="UserObjectGroupM2M", + related_name="user_object_groups", + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "User Object Groups" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:userobjectgroup", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.name + + +########################### +# Through Models +########################### + + +class UserObjectGroupM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated User if assigned to a UserGroup.""" + + user = models.ForeignKey("nautobot_firewall_models.UserObject", on_delete=models.PROTECT) + user_group = models.ForeignKey("nautobot_firewall_models.UserObjectGroup", on_delete=models.CASCADE) diff --git a/nautobot_firewall_models/models/zone.py b/nautobot_firewall_models/models/zone.py new file mode 100644 index 00000000..45a05fa5 --- /dev/null +++ b/nautobot_firewall_models/models/zone.py @@ -0,0 +1,77 @@ +"""Models for the Firewall plugin.""" +# pylint: disable=duplicate-code + +from django.db import models +from django.urls import reverse +from nautobot.core.models.generics import BaseModel, PrimaryModel +from nautobot.extras.models import StatusField +from nautobot.extras.utils import extras_features + +from nautobot_firewall_models.utils import get_default_status + + +########################### +# Core Models +########################### + + +@extras_features( + "custom_fields", + "custom_links", + "custom_validators", + "export_templates", + "graphql", + "relationships", + "statuses", + "webhooks", +) +class Zone(PrimaryModel): + """Zones common on firewalls and are typically seen as representations of area (e.g. DMZ trust untrust).""" + + description = models.CharField( + max_length=200, + blank=True, + ) + name = models.CharField(max_length=100, unique=True, help_text="Name of the zone (e.g. trust)") + vrfs = models.ManyToManyField(to="ipam.VRF", blank=True, through="ZoneVRFM2M", related_name="zones") + interfaces = models.ManyToManyField( + to="dcim.Interface", blank=True, through="ZoneInterfaceM2M", related_name="zones" + ) + status = StatusField( + on_delete=models.PROTECT, + related_name="%(app_label)s_%(class)s_related", # e.g. dcim_device_related + default=get_default_status, + ) + + class Meta: + """Meta class.""" + + ordering = ["name"] + verbose_name_plural = "Zones" + + def get_absolute_url(self): + """Return detail view URL.""" + return reverse("plugins:nautobot_firewall_models:zone", args=[self.pk]) + + def __str__(self): + """Stringify instance.""" + return self.name + + +########################### +# Through Models +########################### + + +class ZoneInterfaceM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated Interface if assigned to a Zone.""" + + zone = models.ForeignKey("nautobot_firewall_models.Zone", on_delete=models.CASCADE) + interface = models.ForeignKey("dcim.Interface", on_delete=models.PROTECT) + + +class ZoneVRFM2M(BaseModel): + """Custom through model to on_delete=models.PROTECT to prevent deleting associated VRF if assigned to a Zone.""" + + zone = models.ForeignKey("nautobot_firewall_models.Zone", on_delete=models.CASCADE) + vrf = models.ForeignKey("ipam.vrf", on_delete=models.PROTECT) diff --git a/nautobot_firewall_models/navigation.py b/nautobot_firewall_models/navigation.py index d4959510..a7bd280c 100644 --- a/nautobot_firewall_models/navigation.py +++ b/nautobot_firewall_models/navigation.py @@ -61,6 +61,28 @@ name="Service", weight=200, items=[ + NavMenuItem( + link="plugins:nautobot_firewall_models:applicationobject_list", + name="Applications", + permissions=["nautobot_firewall_models.view_applicationobject"], + buttons=[ + NavMenuAddButton( + link="plugins:nautobot_firewall_models:applicationobject_add", + permissions=["nautobot_firewall_models.add_applicationobject"], + ), + ], + ), + NavMenuItem( + link="plugins:nautobot_firewall_models:applicationobjectgroup_list", + name="Application Groups", + permissions=["nautobot_firewall_models.view_applicationobjectgroup"], + buttons=[ + NavMenuAddButton( + link="plugins:nautobot_firewall_models:applicationobjectgroup_add", + permissions=["nautobot_firewall_models.add_applicationobjectgroup"], + ), + ], + ), NavMenuItem( link="plugins:nautobot_firewall_models:serviceobject_list", name="Service Objects", diff --git a/nautobot_firewall_models/tables.py b/nautobot_firewall_models/tables.py index 9ee3af04..0bdd135a 100644 --- a/nautobot_firewall_models/tables.py +++ b/nautobot_firewall_models/tables.py @@ -64,6 +64,35 @@ class Meta(BaseTable.Meta): fields = ("pk", "name", "description", "address_objects", "status") +class ApplicationObjectTable(StatusTableMixin, BaseTable): + """Table for list view.""" + + pk = ToggleColumn() + name = tables.Column(linkify=True) + actions = ButtonsColumn(models.ApplicationObject, buttons=("edit", "delete")) + + class Meta(BaseTable.Meta): + """Meta attributes.""" + + model = models.ApplicationObject + fields = ("pk", "name", "description", "category", "subcategory", "technology", "risk", "default_type") + + +class ApplicationObjectGroupTable(StatusTableMixin, BaseTable): + """Table for list view.""" + + pk = ToggleColumn() + name = tables.Column(linkify=True) + application_objects = tables.ManyToManyColumn(linkify_item=True) + actions = ButtonsColumn(models.ApplicationObjectGroup, buttons=("edit", "delete")) + + class Meta(BaseTable.Meta): + """Meta attributes.""" + + model = models.ApplicationObjectGroup + fields = ("pk", "name", "description", "application_objects") + + class ServiceObjectTable(StatusTableMixin, BaseTable): """Table for list view.""" @@ -162,6 +191,8 @@ class Meta(BaseTable.Meta): "destination_zone", "destination_services", "destination_service_groups", + "applications", + "application_groups", "action", "description", "request_id", @@ -183,6 +214,8 @@ class Meta(BaseTable.Meta): "destination_zone", "destination_services", "destination_service_groups", + "applications", + "application_groups", "action", "log", "status", diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/applicationobject.html b/nautobot_firewall_models/templates/nautobot_firewall_models/applicationobject.html new file mode 100644 index 00000000..3ed51b19 --- /dev/null +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/applicationobject.html @@ -0,0 +1,54 @@ +{% extends 'generic/object_detail.html' %} +{% load helpers %} + +{% block content_left_page %} +
+
+ Application Object +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name{{ object.name|placeholder }}
Description{{ object.description|placeholder }}
Category{{ object.category|placeholder }}
Sub-Category{{ object.subcategory|placeholder }}
Technology{{ object.technology|placeholder }}
Risk{{ object.risk|placeholder }}
Default Type{{ object.default_type|placeholder }}
Default IP Protocol{{ object.default_ip_protocol|placeholder }}
Status + {{ object.get_status_display }} +
+
+{% endblock %} diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/applicationobjectgroup.html b/nautobot_firewall_models/templates/nautobot_firewall_models/applicationobjectgroup.html new file mode 100644 index 00000000..92de628a --- /dev/null +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/applicationobjectgroup.html @@ -0,0 +1,36 @@ +{% extends 'generic/object_detail.html' %} +{% load helpers %} + +{% block content_left_page %} +
+
+ Application Object Group +
+ + + + + + + + + + + + + +
Description{{ object.description|placeholder }}
Application Objects +
    + {% for i in object.application_objects.all %} +
  • {{ i|placeholder }}
  • + {% empty %} None {% endfor %} +
+
Status + {{ object.get_status_display }} +
+
+{% endblock %} diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_application_object_row.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_application_object_row.html new file mode 100644 index 00000000..0d618481 --- /dev/null +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policy_rule_application_object_row.html @@ -0,0 +1,12 @@ +{% load helpers %} + +{% if application_group or application %} + {% for i in application_group %} + {{ i|placeholder }}
+ {% endfor %} + {% for i in application %} + {{ i|placeholder }}
+ {% endfor %} +{% else %} + {% if nat %}—{% else %}ANY{% endif %} + {% endif %} 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 b7c77864..00755915 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 @@ -2,7 +2,7 @@ {{ 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_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 %} @@ -11,6 +11,7 @@ {% 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.destination_services.all service_group=m2m.rule.destination_service_groups.all %} + {% include './policy_rule_application_object_row.html' with application=m2m.rule.applications.all application_group=m2m.rule.application_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/policyrule_tablehead.html b/nautobot_firewall_models/templates/nautobot_firewall_models/inc/policyrule_tablehead.html index 50a6c8f9..c69ef05a 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,7 +1,7 @@ Index Source - Destination + Destination Action Log @@ -13,4 +13,5 @@ Address Zone Service + Application \ 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 9d6083aa..83f91467 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 @@ -2,7 +2,7 @@ {{ rule.index }} {% if rule.action == "remark" %} - {{ rule }} + {{ rule }} {% else %} {% 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 %} @@ -11,6 +11,7 @@ {% 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.destination_services.all service_group=rule.destination_service_groups.all %} + {% include './policy_rule_application_object_row.html' with application=rule.applications.all application_group=rule.application_groups.all %} {% endif %} {% include './policy_rule_action_row.html' with action=rule.action %} diff --git a/nautobot_firewall_models/templates/nautobot_firewall_models/natpolicy_retrieve.html b/nautobot_firewall_models/templates/nautobot_firewall_models/natpolicy_retrieve.html index 1907c6b2..45760d10 100644 --- a/nautobot_firewall_models/templates/nautobot_firewall_models/natpolicy_retrieve.html +++ b/nautobot_firewall_models/templates/nautobot_firewall_models/natpolicy_retrieve.html @@ -1,7 +1,5 @@ {% extends 'generic/object_retrieve.html' %} {% load helpers %} -{% load plugins %} - {% block extra_nav_tabs %}