diff --git a/tests/munki/test_api_views.py b/tests/munki/test_api_views.py index edfde69782..abea209cef 100644 --- a/tests/munki/test_api_views.py +++ b/tests/munki/test_api_views.py @@ -189,40 +189,77 @@ def test_create_configuration_default_values(self): self.assertFalse(configuration.auto_failed_install_incidents) self.assertEqual(configuration.version, 0) - def test_create_configuration(self): + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_create_configuration(self, post_event): self.set_permissions("munki.add_configuration") name = get_random_string(12) - response = self.post( - reverse("munki_api:configurations"), - {"name": name, - "description": "Description", - "inventory_apps_full_info_shard": 50, - "principal_user_detection_sources": ["google_chrome", "company_portal"], - "principal_user_detection_domains": ["zentral.io"], - "collected_condition_keys": ["yolo"], - "managed_installs_sync_interval_days": 1, - "script_checks_run_interval_seconds": 86400, - "auto_reinstall_incidents": True, - "auto_failed_install_incidents": True} - ) + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.post( + reverse("munki_api:configurations"), + {"name": name, + "description": "Description", + "inventory_apps_full_info_shard": 50, + "principal_user_detection_sources": ["google_chrome", "company_portal"], + "principal_user_detection_domains": ["zentral.io"], + "collected_condition_keys": ["yolo"], + "managed_installs_sync_interval_days": 1, + "script_checks_run_interval_seconds": 86400, + "auto_reinstall_incidents": True, + "auto_failed_install_incidents": True} + ) self.assertEqual(response.status_code, 201) + self.assertEqual(len(callbacks), 1) configuration = Configuration.objects.get(name=name) + event = post_event.call_args_list[0].args[0] + self.assertIsInstance(event, AuditEvent) + self.assertEqual( + event.payload, + { + "action": "created", + "object": { + "model": "munki.configuration", + "pk": str(configuration.pk), + "new_value": { + 'pk': configuration.pk, + 'name': name, + 'description': 'Description', + 'inventory_apps_full_info_shard': 50, + 'principal_user_detection_sources': ["google_chrome", "company_portal"], + 'principal_user_detection_domains': ["zentral.io"], + 'collected_condition_keys': ["yolo"], + 'managed_installs_sync_interval_days': 1, + 'script_checks_run_interval_seconds': 86400, + 'auto_reinstall_incidents': True, + 'auto_failed_install_incidents': True, + 'created_at': configuration.created_at, + 'updated_at': configuration.updated_at, + 'version': 0, + } + } + } + ) + + metadata = event.metadata.serialize() + self.assertEqual(metadata["objects"], {"munki_configuration": [str(configuration.pk)]}) + self.assertEqual(sorted(metadata["tags"]), ["munki", "zentral"]) self.assertEqual( response.json(), - {'id': configuration.pk, - 'name': name, - 'description': 'Description', - 'inventory_apps_full_info_shard': 50, - 'principal_user_detection_sources': ["google_chrome", "company_portal"], - 'principal_user_detection_domains': ["zentral.io"], - 'collected_condition_keys': ["yolo"], - 'managed_installs_sync_interval_days': 1, - 'script_checks_run_interval_seconds': 86400, - 'auto_reinstall_incidents': True, - 'auto_failed_install_incidents': True, - 'version': 0, - 'created_at': configuration.created_at.isoformat(), - 'updated_at': configuration.updated_at.isoformat()} + { + 'id': configuration.pk, + 'name': name, + 'description': 'Description', + 'inventory_apps_full_info_shard': 50, + 'principal_user_detection_sources': ["google_chrome", "company_portal"], + 'principal_user_detection_domains': ["zentral.io"], + 'collected_condition_keys': ["yolo"], + 'managed_installs_sync_interval_days': 1, + 'script_checks_run_interval_seconds': 86400, + 'auto_reinstall_incidents': True, + 'auto_failed_install_incidents': True, + 'version': 0, + 'created_at': configuration.created_at.isoformat(), + 'updated_at': configuration.updated_at.isoformat() + } ) self.assertEqual(configuration.name, name) self.assertEqual(configuration.description, "Description") @@ -282,41 +319,98 @@ def test_update_configuration_permission_denied(self): response = self.put(reverse("munki_api:configuration", args=(configuration.pk,)), {}) self.assertEqual(response.status_code, 403) - def test_update_configuration(self): + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_update_configuration(self, post_event): configuration = self.force_configuration() self.set_permissions("munki.change_configuration") + prev_name = configuration.name + prev_updated_at = configuration.updated_at name = get_random_string(12) - response = self.put( - reverse("munki_api:configuration", args=(configuration.pk,)), - {"name": name, - "description": "Description", - "inventory_apps_full_info_shard": 50, - "principal_user_detection_sources": ["google_chrome", "company_portal"], - "principal_user_detection_domains": ["zentral.io"], - "collected_condition_keys": ["yolo"], - "managed_installs_sync_interval_days": 1, - "script_checks_run_interval_seconds": 86400, - "auto_reinstall_incidents": True, - "auto_failed_install_incidents": True} - ) + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.put( + reverse("munki_api:configuration", args=(configuration.pk,)), + { + "name": name, + "description": "Description", + "inventory_apps_full_info_shard": 50, + "principal_user_detection_sources": ["google_chrome", "company_portal"], + "principal_user_detection_domains": ["zentral.io"], + "collected_condition_keys": ["yolo"], + "managed_installs_sync_interval_days": 1, + "script_checks_run_interval_seconds": 86400, + "auto_reinstall_incidents": True, + "auto_failed_install_incidents": True} + ) self.assertEqual(response.status_code, 200) + self.assertEqual(len(callbacks), 1) configuration.refresh_from_db() + event = post_event.call_args_list[0].args[0] + self.assertIsInstance(event, AuditEvent) + self.assertEqual( + event.payload, + { + "action": "updated", + "object": + { + "model": "munki.configuration", + "pk": str(configuration.pk), + "prev_value": { + 'pk': configuration.pk, + 'name': prev_name, + 'description': '', + 'inventory_apps_full_info_shard': 100, + 'principal_user_detection_sources': [], + 'principal_user_detection_domains': [], + 'collected_condition_keys': [], + 'managed_installs_sync_interval_days': 7, + 'script_checks_run_interval_seconds': 86400, + 'auto_reinstall_incidents': False, + 'auto_failed_install_incidents': False, + 'version': 0, + 'created_at': configuration.created_at, + 'updated_at': prev_updated_at + }, + "new_value": { + 'pk': configuration.pk, + 'name': name, + 'description': 'Description', + 'inventory_apps_full_info_shard': 50, + 'principal_user_detection_sources': ["google_chrome", "company_portal"], + 'principal_user_detection_domains': ["zentral.io"], + 'collected_condition_keys': ["yolo"], + 'managed_installs_sync_interval_days': 1, + 'script_checks_run_interval_seconds': 86400, + 'auto_reinstall_incidents': True, + 'auto_failed_install_incidents': True, + 'version': 1, + 'created_at': configuration.created_at, + 'updated_at': configuration.updated_at + } + } + } + ) + + metadata = event.metadata.serialize() + self.assertEqual(metadata["objects"], {"munki_configuration": [str(configuration.pk)]}) + self.assertEqual(sorted(metadata["tags"]), ["munki", "zentral"]) self.assertEqual( response.json(), - {'id': configuration.pk, - 'name': name, - 'description': 'Description', - 'inventory_apps_full_info_shard': 50, - 'principal_user_detection_sources': ["google_chrome", "company_portal"], - 'principal_user_detection_domains': ["zentral.io"], - 'collected_condition_keys': ["yolo"], - 'managed_installs_sync_interval_days': 1, - 'script_checks_run_interval_seconds': 86400, - 'auto_reinstall_incidents': True, - 'auto_failed_install_incidents': True, - 'version': 1, - 'created_at': configuration.created_at.isoformat(), - 'updated_at': configuration.updated_at.isoformat()} + { + 'id': configuration.pk, + 'name': name, + 'description': 'Description', + 'inventory_apps_full_info_shard': 50, + 'principal_user_detection_sources': ["google_chrome", "company_portal"], + 'principal_user_detection_domains': ["zentral.io"], + 'collected_condition_keys': ["yolo"], + 'managed_installs_sync_interval_days': 1, + 'script_checks_run_interval_seconds': 86400, + 'auto_reinstall_incidents': True, + 'auto_failed_install_incidents': True, + 'version': 1, + 'created_at': configuration.created_at.isoformat(), + 'updated_at': configuration.updated_at.isoformat() + } ) self.assertEqual(configuration.name, name) self.assertEqual(configuration.description, "Description") @@ -342,12 +436,47 @@ def test_delete_configuration_permission_denied(self): response = self.delete(reverse("munki_api:configuration", args=(configuration.pk,))) self.assertEqual(response.status_code, 403) - def test_delete_configuration(self): + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_delete_configuration(self, post_event): configuration = self.force_configuration() + prev_pk = configuration.pk self.set_permissions("munki.delete_configuration") - response = self.delete(reverse("munki_api:configuration", args=(configuration.pk,))) + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.delete(reverse("munki_api:configuration", args=(configuration.pk,))) self.assertEqual(response.status_code, 204) + self.assertEqual(len(callbacks), 1) self.assertEqual(Configuration.objects.filter(pk=configuration.pk).count(), 0) + event = post_event.call_args_list[0].args[0] + self.assertIsInstance(event, AuditEvent) + self.assertEqual( + event.payload, + { + "action": "deleted", + "object": { + "model": "munki.configuration", + "pk": str(prev_pk), + "prev_value": { + 'pk': prev_pk, + 'name': configuration.name, + 'description': '', + 'inventory_apps_full_info_shard': 100, + 'principal_user_detection_sources': [], + 'principal_user_detection_domains': [], + 'collected_condition_keys': [], + 'managed_installs_sync_interval_days': 7, + 'script_checks_run_interval_seconds': 86400, + 'auto_reinstall_incidents': False, + 'auto_failed_install_incidents': False, + 'version': 0, + 'created_at': configuration.created_at, + 'updated_at': configuration.updated_at + } + } + } + ) + metadata = event.metadata.serialize() + self.assertEqual(metadata["objects"], {"munki_configuration": [str(prev_pk)]}) + self.assertEqual(sorted(metadata["tags"]), ["munki", "zentral"]) # list enrollments diff --git a/tests/munki/test_setup_views.py b/tests/munki/test_setup_views.py index 1d1cecdf6c..55a5047f7b 100644 --- a/tests/munki/test_setup_views.py +++ b/tests/munki/test_setup_views.py @@ -9,14 +9,18 @@ from django.urls import reverse from django.utils.crypto import get_random_string from django.test import TestCase, override_settings +from unittest.mock import patch from zentral.contrib.inventory.models import MetaBusinessUnit, Tag from zentral.contrib.munki.models import Enrollment from accounts.models import User +from zentral.core.events.base import AuditEvent from .utils import force_configuration, force_enrollment, force_script_check, make_enrolled_machine @override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage') class MunkiSetupViewsTestCase(TestCase): + maxDiff = None + @classmethod def setUpTestData(cls): # user @@ -107,6 +111,55 @@ def test_configuration_enrollment_and_machine_count(self): self.assertEqual(response.context['object_list'][0].enrollment__count, 2) self.assertEqual(response.context['object_list'][0].enrollment__enrolledmachine__count, 3) + def test_configuration_without_event_links(self): + configuration = force_configuration() + self._login("munki.view_configuration") + response = self.client.get(configuration.get_absolute_url()) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "munki/configuration_detail.html") + self.assertNotContains(response, reverse("munki:configuration_events", + args=(configuration.pk,))) + + def test_configuration_with_event_links(self): + configuration = force_configuration() + self._login("munki.view_configuration", + "munki.view_enrollment") + response = self.client.get(configuration.get_absolute_url()) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "munki/configuration_detail.html") + self.assertContains(response, reverse("munki:configuration_events", + args=(configuration.pk,))) + + def test_configuration_events_redirect(self): + configuration = force_configuration() + self._login_redirect(reverse("munki:configuration_events", args=(configuration.pk,))) + + def test_configuration_events_permission_denied(self): + configuration = force_configuration() + self._login("munki.view_configuration") + response = self.client.get(reverse("munki:configuration_events", args=(configuration.pk,))) + self.assertEqual(response.status_code, 403) + + @patch("zentral.core.stores.backends.elasticsearch.EventStore.get_aggregated_object_event_counts") + def test_configuration_events_ok(self, get_aggregated_object_event_counts): + get_aggregated_object_event_counts.return_value = {} + configuration = force_configuration() + self._login("munki.view_configuration", + "munki.view_enrollment") + response = self.client.get(reverse("munki:configuration_events", args=(configuration.pk,))) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "munki/configuration_events.html") + + @patch("zentral.core.stores.backends.elasticsearch.EventStore.fetch_object_events") + def test_fetch_configuration_events_ok(self, fetch_object_events): + fetch_object_events.return_value = {} + configuration = force_configuration() + self._login("munki.view_configuration", + "munki.view_enrollment") + response = self.client.get(reverse("munki:configuration_events", args=(configuration.pk,))) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "munki/configuration_events.html") + # create configuration def test_create_configuration_redirect(self): @@ -123,23 +176,26 @@ def test_create_configuration_get(self): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "munki/configuration_form.html") - def test_create_configuration_post(self): + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_create_configuration_post(self, post_event): self._login("munki.add_configuration", "munki.view_configuration") name = get_random_string(12) description = get_random_string(12) collected_condition_keys = sorted(get_random_string(12) for _ in range(3)) - response = self.client.post(reverse("munki:create_configuration"), - {"name": name, - "description": description, - "inventory_apps_full_info_shard": 17, - "principal_user_detection_sources": "logged_in_user", - "principal_user_detection_domains": "yolo.fr", - "collected_condition_keys": " , ".join(collected_condition_keys), - "managed_installs_sync_interval_days": 1, - "script_checks_run_interval_seconds": 7231, - "auto_reinstall_incidents": "on"}, - follow=True) + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.client.post(reverse("munki:create_configuration"), + {"name": name, + "description": description, + "inventory_apps_full_info_shard": 17, + "principal_user_detection_sources": "logged_in_user", + "principal_user_detection_domains": "yolo.fr", + "collected_condition_keys": " , ".join(collected_condition_keys), + "managed_installs_sync_interval_days": 1, + "script_checks_run_interval_seconds": 7231, + "auto_reinstall_incidents": "on"}, + follow=True) self.assertEqual(response.status_code, 200) + self.assertEqual(len(callbacks), 1) self.assertTemplateUsed(response, "munki/configuration_detail.html") self.assertContains(response, name) self.assertContains(response, description) @@ -153,6 +209,38 @@ def test_create_configuration_post(self): self.assertEqual(sorted(configuration.collected_condition_keys), collected_condition_keys) for condition_key in collected_condition_keys: self.assertContains(response, condition_key) + event = post_event.call_args_list[0].args[0] + self.assertIsInstance(event, AuditEvent) + self.assertEqual( + event.payload, + { + "action": "created", + "object": + { + "model": "munki.configuration", + "pk": str(configuration.pk), + "new_value": { + "pk": configuration.pk, + "name": name, + "description": description, + "inventory_apps_full_info_shard": 17, + "principal_user_detection_sources": ["logged_in_user"], + "principal_user_detection_domains": ["yolo.fr"], + "collected_condition_keys": collected_condition_keys, + "managed_installs_sync_interval_days": 1, + "script_checks_run_interval_seconds": 7231, + "auto_reinstall_incidents": True, + "auto_failed_install_incidents": False, + "created_at": configuration.created_at, + "updated_at": configuration.updated_at, + "version": 0, + } + } + } + ) + metadata = event.metadata.serialize() + self.assertEqual(metadata["objects"], {"munki_configuration": [str(configuration.pk)]}) + self.assertEqual(sorted(metadata["tags"]), ["munki", "zentral"]) # update configuration @@ -173,21 +261,27 @@ def test_update_configuration_get(self): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "munki/configuration_form.html") - def test_update_configuration_post(self): + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_update_configuration_post(self, post_event): configuration = force_configuration() + prev_updated_at = configuration.updated_at self._login("munki.change_configuration", "munki.view_configuration") collected_condition_keys = sorted(get_random_string(12) for _ in range(3)) - response = self.client.post(reverse("munki:update_configuration", args=(configuration.pk,)), - {"name": configuration.name, - "inventory_apps_full_info_shard": 17, - "principal_user_detection_sources": "logged_in_user", - "principal_user_detection_domains": "yolo.fr", - "collected_condition_keys": ",".join(collected_condition_keys), - "managed_installs_sync_interval_days": 2, - "script_checks_run_interval_seconds": 3600, - "auto_failed_install_incidents": "on"}, - follow=True) + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.client.post(reverse("munki:update_configuration", args=(configuration.pk,)), + { + "name": configuration.name, + "inventory_apps_full_info_shard": 17, + "principal_user_detection_sources": "logged_in_user", + "principal_user_detection_domains": "yolo.fr", + "collected_condition_keys": ",".join(collected_condition_keys), + "managed_installs_sync_interval_days": 2, + "script_checks_run_interval_seconds": 3600, + "auto_failed_install_incidents": "on"}, + follow=True + ) self.assertEqual(response.status_code, 200) + self.assertEqual(len(callbacks), 1) self.assertTemplateUsed(response, "munki/configuration_detail.html") configuration2 = response.context["object"] self.assertEqual(configuration2, configuration) @@ -200,6 +294,54 @@ def test_update_configuration_post(self): self.assertEqual(sorted(configuration2.collected_condition_keys), collected_condition_keys) for condition_key in collected_condition_keys: self.assertContains(response, condition_key) + event = post_event.call_args_list[0].args[0] + self.assertIsInstance(event, AuditEvent) + self.assertEqual( + event.payload, + { + "action": "updated", + "object": + { + "model": "munki.configuration", + "pk": str(configuration.pk), + "prev_value": { + "pk": configuration.pk, + "name": configuration.name, + "description": "", + "inventory_apps_full_info_shard": 100, + "principal_user_detection_sources": [], + "principal_user_detection_domains": [], + "collected_condition_keys": [], + "managed_installs_sync_interval_days": 7, + "script_checks_run_interval_seconds": 86400, + "auto_failed_install_incidents": False, + "auto_reinstall_incidents": False, + "created_at": configuration.created_at, + "updated_at": prev_updated_at, + "version": 0, + }, + "new_value": { + "pk": configuration2.pk, + "description": "", + "name": configuration2.name, + "inventory_apps_full_info_shard": 17, + "principal_user_detection_sources": ["logged_in_user"], + "principal_user_detection_domains": ["yolo.fr"], + "collected_condition_keys": collected_condition_keys, + "managed_installs_sync_interval_days": 2, + "script_checks_run_interval_seconds": 3600, + "auto_failed_install_incidents": True, + "auto_reinstall_incidents": False, + "created_at": configuration2.created_at, + "updated_at": configuration2.updated_at, + "version": configuration2.version, + } + } + } + ) + metadata = event.metadata.serialize() + self.assertEqual(metadata["objects"], {"munki_configuration": [str(configuration.pk)]}) + self.assertEqual(sorted(metadata["tags"]), ["munki", "zentral"]) # create enrollment diff --git a/zentral/contrib/munki/api_views.py b/zentral/contrib/munki/api_views.py index 0ec0482b24..68ca912b95 100644 --- a/zentral/contrib/munki/api_views.py +++ b/zentral/contrib/munki/api_views.py @@ -12,7 +12,7 @@ # configurations -class ConfigurationList(generics.ListCreateAPIView): +class ConfigurationList(ListCreateAPIViewWithAudit): queryset = Configuration.objects.all() serializer_class = ConfigurationSerializer permission_classes = [DefaultDjangoModelPermissions] @@ -20,7 +20,7 @@ class ConfigurationList(generics.ListCreateAPIView): filterset_fields = ("name",) -class ConfigurationDetail(generics.RetrieveUpdateDestroyAPIView): +class ConfigurationDetail(RetrieveUpdateDestroyAPIViewWithAudit): queryset = Configuration.objects.all() serializer_class = ConfigurationSerializer permission_classes = [DefaultDjangoModelPermissions] diff --git a/zentral/contrib/munki/models.py b/zentral/contrib/munki/models.py index c9ee326c16..d5bfa00c7d 100644 --- a/zentral/contrib/munki/models.py +++ b/zentral/contrib/munki/models.py @@ -87,6 +87,29 @@ def save(self, *args, **kwargs): self.version = F("version") + 1 super().save(*args, **kwargs) + def serialize_for_event(self, keys_only=False): + d = {"pk": self.pk, "name": self.name} + if not keys_only: + if not isinstance(self.version, int): + # version was updated with a CombinedExpression + # it needs to be fetched from the DB for the JSON serialization + self.refresh_from_db() + d.update({ + "description": self.description, + "inventory_apps_full_info_shard": self.inventory_apps_full_info_shard, + "principal_user_detection_sources": self.principal_user_detection_sources, + "principal_user_detection_domains": self.principal_user_detection_domains, + "collected_condition_keys": self.collected_condition_keys, + "managed_installs_sync_interval_days": self.managed_installs_sync_interval_days, + "script_checks_run_interval_seconds": self.script_checks_run_interval_seconds, + "auto_reinstall_incidents": self.auto_reinstall_incidents, + "auto_failed_install_incidents": self.auto_failed_install_incidents, + "created_at": self.created_at, + "updated_at": self.updated_at, + "version": self.version, + }) + return d + # enrollment @@ -100,6 +123,11 @@ def get_absolute_url(self): def get_description_for_distributor(self): return "Zentral pre/postflight" + def serialize_for_event(self): + enrollment_dict = super().serialize_for_event() + enrollment_dict["configuration"] = self.configuration.serialize_for_event(keys_only=True) + return enrollment_dict + class EnrolledMachine(models.Model): enrollment = models.ForeignKey(Enrollment, on_delete=models.CASCADE) diff --git a/zentral/contrib/munki/templates/munki/configuration_detail.html b/zentral/contrib/munki/templates/munki/configuration_detail.html index 6aacf6a6d7..fca2056dcf 100644 --- a/zentral/contrib/munki/templates/munki/configuration_detail.html +++ b/zentral/contrib/munki/templates/munki/configuration_detail.html @@ -13,10 +13,17 @@