From c0694d989f25c16a6b23f4f971184f185082b7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Falconnier?= Date: Fri, 22 Nov 2024 11:52:02 +0100 Subject: [PATCH] Add delete Santa configuration view --- tests/santa/test_setup_views.py | 94 ++++++++++++++++++- zentral/contrib/santa/models.py | 10 +- .../santa/configuration_confirm_delete.html | 23 +++++ .../templates/santa/configuration_detail.html | 4 + zentral/contrib/santa/urls.py | 3 + zentral/contrib/santa/views/setup.py | 11 ++- 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 zentral/contrib/santa/templates/santa/configuration_confirm_delete.html diff --git a/tests/santa/test_setup_views.py b/tests/santa/test_setup_views.py index 589c7b2738..096eea7490 100644 --- a/tests/santa/test_setup_views.py +++ b/tests/santa/test_setup_views.py @@ -209,7 +209,7 @@ def test_configuration_without_event_links(self): self.assertNotContains(response, reverse("santa:configuration_events_store_redirect", args=(configuration.pk,))) - def test_configuration_with_event_links(self): + def test_configuration_some_links(self): configuration = force_configuration() self._login("santa.view_configuration", "santa.view_enrollment", @@ -220,6 +220,28 @@ def test_configuration_with_event_links(self): self.assertTemplateUsed(response, "santa/configuration_detail.html") self.assertContains(response, reverse("santa:configuration_events", args=(configuration.pk,))) + self.assertNotContains(response, reverse("santa:update_configuration", + args=(configuration.pk,))) + self.assertNotContains(response, reverse("santa:delete_configuration", + args=(configuration.pk,))) + + def test_configuration_all_links(self): + configuration = force_configuration() + self._login("santa.view_configuration", + "santa.change_configuration", + "santa.delete_configuration", + "santa.view_enrollment", + "santa.view_rule", + "santa.view_ruleset") + response = self.client.get(configuration.get_absolute_url()) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "santa/configuration_detail.html") + self.assertContains(response, reverse("santa:configuration_events", + args=(configuration.pk,))) + self.assertContains(response, reverse("santa:update_configuration", + args=(configuration.pk,))) + self.assertContains(response, reverse("santa:delete_configuration", + args=(configuration.pk,))) def test_configuration_events_redirect(self): configuration = force_configuration() @@ -482,6 +504,76 @@ def test_post_update_configuration_view_remount_usb_mode_error(self): self.assertFormError(response.context["form"], "remount_usb_mode", "'Block USB mount' must be set to use this option") + # delete configuration + + def test_delete_configuration_redirect(self): + configuration = force_configuration() + self._login_redirect(reverse("santa:delete_configuration", args=(configuration.pk,))) + + def test_post_delete_configuration_view_permission_denied(self): + self._login("santa.add_configuration", "santa.view_configuration") + configuration = force_configuration() + response = self.client.post(reverse("santa:delete_configuration", args=(configuration.pk,)), + follow=True) + self.assertEqual(response.status_code, 403) + + def test_post_delete_configuration_cannot_be_deleted(self): + _, enrollment = self._force_enrollment() + self._login("santa.delete_configuration", "santa.view_configuration") + response = self.client.post(reverse("santa:delete_configuration", args=(enrollment.configuration.pk,)), + follow=True) + self.assertEqual(response.status_code, 404) + + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_post_delete_configuration_view(self, post_event): + configuration = force_configuration() + configuration_pk = configuration.pk + self._login("santa.delete_configuration", "santa.view_configuration") + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.client.post(reverse("santa:delete_configuration", args=(configuration.pk,)), + follow=True) + self.assertTemplateUsed(response, "santa/configuration_list.html") + self.assertEqual(len(callbacks), 1) + self.assertNotContains(response, configuration.name) + event = post_event.call_args_list[0].args[0] + self.assertIsInstance(event, AuditEvent) + self.assertEqual( + event.payload, + {"action": "deleted", + "object": { + "model": "santa.configuration", + "pk": str(configuration_pk), + "prev_value": { + "pk": configuration_pk, + "name": configuration.name, + "client_mode": "Monitor", + "client_certificate_auth": False, + "batch_size": 50, + "full_sync_interval": 600, + "enable_bundles": False, + "enable_transitive_rules": False, + "allowed_path_regex": "", + "blocked_path_regex": "", + "block_usb_mount": False, + "remount_usb_mode": [], + "allow_unknown_shard": 100, + "enable_all_event_upload_shard": 0, + "sync_incident_severity": 0, + "voting_realm": None, + "banned_threshold": -26, + "default_ballot_target_types": [], + "default_voting_weight": 0, + "globally_allowlisted_threshold": 50, + "partially_allowlisted_threshold": 5, + "created_at": configuration.created_at, + "updated_at": configuration.updated_at + }, + }} + ) + metadata = event.metadata.serialize() + self.assertEqual(metadata["objects"], {"santa_configuration": [str(configuration_pk)]}) + self.assertEqual(sorted(metadata["tags"]), ["santa", "zentral"]) + # enrollment def test_create_enrollment_redirect(self): diff --git a/zentral/contrib/santa/models.py b/zentral/contrib/santa/models.py index 383a6bbc57..e7b9d4a356 100644 --- a/zentral/contrib/santa/models.py +++ b/zentral/contrib/santa/models.py @@ -484,6 +484,14 @@ def summary(self): columns = [c.name for c in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] + def for_deletion(self): + return self.annotate( + # no enrollments + enrollment_count=Count("enrollment") + ).filter( + enrollment_count=0 + ) + class Configuration(models.Model): MONITOR_MODE = 1 @@ -731,7 +739,7 @@ def serialize_for_event(self, keys_only=False): return d def can_be_deleted(self): - return self.enrollment_set.all().count() == 0 + return Configuration.objects.for_deletion().filter(pk=self.pk).exists() class VotingGroup(models.Model): diff --git a/zentral/contrib/santa/templates/santa/configuration_confirm_delete.html b/zentral/contrib/santa/templates/santa/configuration_confirm_delete.html new file mode 100644 index 0000000000..477c01fdae --- /dev/null +++ b/zentral/contrib/santa/templates/santa/configuration_confirm_delete.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block content %} + + +

Delete Santa configuration

+ +
{% csrf_token %} +

Do you really want to delete this Santa configuration?

+

+ + Cancel + + +

+
+{% endblock %} diff --git a/zentral/contrib/santa/templates/santa/configuration_detail.html b/zentral/contrib/santa/templates/santa/configuration_detail.html index 363183d16b..e6ca7d5e80 100644 --- a/zentral/contrib/santa/templates/santa/configuration_detail.html +++ b/zentral/contrib/santa/templates/santa/configuration_detail.html @@ -36,6 +36,10 @@

Santa configuration

{% url 'santa:update_configuration' object.pk as url %} {% button 'UPDATE' url "Edit Configuration" %} {% endif %} + {% if perms.santa.delete_configuration and object.can_be_deleted %} + {% url 'santa:delete_configuration' object.pk as url %} + {% button 'DELETE' url "Delete Configuration" %} + {% endif %} diff --git a/zentral/contrib/santa/urls.py b/zentral/contrib/santa/urls.py index 9e7617b710..856a6e63f4 100644 --- a/zentral/contrib/santa/urls.py +++ b/zentral/contrib/santa/urls.py @@ -28,6 +28,9 @@ path('configurations//update/', views.UpdateConfigurationView.as_view(), name='update_configuration'), + path('configurations//delete/', + views.DeleteConfigurationView.as_view(), + name='delete_configuration'), path('configurations//enrollments/create/', views.CreateEnrollmentView.as_view(), name='create_enrollment'), diff --git a/zentral/contrib/santa/views/setup.py b/zentral/contrib/santa/views/setup.py index 371ec24458..d2b6720278 100644 --- a/zentral/contrib/santa/views/setup.py +++ b/zentral/contrib/santa/views/setup.py @@ -4,7 +4,7 @@ from django.db import transaction from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.views.generic import DetailView, TemplateView, View from django.views.generic.edit import DeleteView, FormView, UpdateView from zentral.contrib.inventory.forms import EnrollmentSecretForm @@ -143,6 +143,15 @@ class UpdateConfigurationView(PermissionRequiredMixin, UpdateViewWithAudit): form_class = ConfigurationForm +class DeleteConfigurationView(PermissionRequiredMixin, DeleteViewWithAudit): + permission_required = "santa.delete_configuration" + model = Configuration + success_url = reverse_lazy("santa:configuration_list") + + def get_queryset(self): + return self.model.objects.for_deletion() + + # voting groups