diff --git a/server/realms/models.py b/server/realms/models.py index e9ccca8f7b..86f8042766 100644 --- a/server/realms/models.py +++ b/server/realms/models.py @@ -260,6 +260,26 @@ def groups_with_types(self): groups_with_types.append((realm_group, raw_groups[realm_group.pk])) return groups_with_types + def serialize_for_event(self, keys_only=False): + d = { + "pk": str(self.uuid), + "realm": self.realm.serialize_for_event(keys_only=True), + "username": self.username, + } + if keys_only: + return d + for attr in ("username", + "email", + "first_name", + "last_name", + "full_name", + "custom_attr_1", + "custom_attr_2"): + val = getattr(self, attr) + if val: + d[attr] = val + return d + class RealmUserGroupMembership(models.Model): user = models.ForeignKey(RealmUser, on_delete=models.CASCADE) diff --git a/tests/santa/test_ballots_views.py b/tests/santa/test_ballots_views.py index b369b32c7c..217a472616 100644 --- a/tests/santa/test_ballots_views.py +++ b/tests/santa/test_ballots_views.py @@ -14,6 +14,7 @@ from realms.backends.views import finalize_session from realms.models import RealmAuthenticationSession from zentral.contrib.santa.ballot_box import DuplicateVoteError, VotingNotAllowedError +from zentral.contrib.santa.events import SantaBallotEvent, SantaTargetStateUpdateEvent from zentral.contrib.santa.models import Ballot, Target, TargetState from .utils import add_file_to_test_class, force_ballot, force_configuration, force_realm, force_realm_user @@ -32,6 +33,7 @@ def setUpTestData(cls): cls.configuration = force_configuration( voting_realm=cls.realm, default_ballot_target_types=[Target.Type.METABUNDLE, Target.Type.BUNDLE, Target.Type.BINARY], + default_voting_weight=1, ) # utility methods @@ -387,18 +389,31 @@ def test_cast_ballot_post_voting_not_allowed(self, cast_votes): self.assertContains(response, "The ballot was rejected") self.assertEqual(ballot_qs.count(), 0) - def test_cast_ballot_post_yes(self): + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_cast_ballot_post_yes(self, post_event): ballot_qs = Ballot.objects.filter(target=self.metabundle_target) self.assertEqual(ballot_qs.count(), 0) self._login("santa.add_ballot", "santa.view_target", realm_user=True) - response = self.client.post(reverse("santa:cast_ballot") - + f"?target_type=METABUNDLE&target_identifier={self.metabundle_sha256}", - {f"cfg-{self.configuration.pk}-yes_no": "YES"}, - follow=True) + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.client.post(reverse("santa:cast_ballot") + + f"?target_type=METABUNDLE&target_identifier={self.metabundle_sha256}", + {f"cfg-{self.configuration.pk}-yes_no": "YES"}, + follow=True) self.assertEqual(response.status_code, 200) + self.assertEqual(len(callbacks), 1) self.assertTemplateUsed(response, "santa/target_detail.html") self.assertContains(response, "Your ballot has been cast") self.assertEqual(ballot_qs.count(), 1) + self.assertEqual(len(post_event.call_args_list), 3) + event1 = post_event.call_args_list[1].args[0] + self.assertIsInstance(event1, SantaBallotEvent) + self.assertEqual(len(event1.payload["votes"]), 1) + self.assertTrue(event1.payload["votes"][0]["was_yes_vote"]) + self.assertEqual(event1.payload["votes"][0]["weight"], 1) + event2 = post_event.call_args_list[2].args[0] + self.assertIsInstance(event2, SantaTargetStateUpdateEvent) + self.assertEqual(event2.payload["new_value"]["score"], 1) + self.assertEqual(event2.payload["prev_value"]["score"], 0) def test_cast_ballot_post_no(self): ballot_qs = Ballot.objects.filter(target=self.file_target) diff --git a/tests/santa/test_targets_views.py b/tests/santa/test_targets_views.py index 806ce9fad5..7a27c70146 100644 --- a/tests/santa/test_targets_views.py +++ b/tests/santa/test_targets_views.py @@ -895,16 +895,44 @@ def test_rest_target_state_post_not_allowed(self): ts.refresh_from_db() self.assertIsNone(ts.reset_at) - def test_rest_target_state_post_allowed(self): + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_rest_target_state_post_allowed(self, post_event): configuration = force_configuration() ts = TargetState.objects.create(configuration=configuration, target=self.file_target) self._login("santa.view_target", realm_user=True) force_voting_group(configuration, self.realm_user, can_reset_target=True) - response = self.client.post(reverse("santa:reset_target_state", args=(configuration.pk, ts.pk)), - follow=True) + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.client.post(reverse("santa:reset_target_state", args=(configuration.pk, ts.pk)), + follow=True) self.assertEqual(response.status_code, 200) + self.assertEqual(len(callbacks), 1) self.assertTemplateUsed(response, "santa/target_detail.html") self.assertContains(response, "Target state reset") self.assertNotContains(response, "Target state reset not allowed") ts.refresh_from_db() self.assertIsNotNone(ts.reset_at) + self.assertEqual(len(post_event.call_args_list), 2) + event = post_event.call_args_list[1].args[0] + self.assertEqual( + event.payload, + {'configuration': {'name': configuration.name, 'pk': configuration.pk}, + 'created_at': ts.created_at, + 'new_value': {'flagged': False, + 'reset_at': ts.reset_at, + 'score': 0, + 'state': 0, + 'state_display': 'UNTRUSTED'}, + 'prev_value': {'flagged': False, + 'reset_at': None, + 'score': 0, + 'state': 0, + 'state_display': 'UNTRUSTED'}, + 'target': {'sha256': self.file_sha256, + 'type': 'BINARY'}, + 'updated_at': ts.updated_at} + ) + self.assertEqual( + event.metadata.serialize()["objects"], + {'file': [f'sha256|{self.file_sha256}'], + 'santa_configuration': [str(configuration.pk)]}, + ) diff --git a/tests/santa/test_up_views.py b/tests/santa/test_up_views.py index 706df6f575..19313243d1 100644 --- a/tests/santa/test_up_views.py +++ b/tests/santa/test_up_views.py @@ -1,5 +1,6 @@ from datetime import datetime from importlib import import_module +from unittest.mock import patch import uuid from django.conf import settings from django.http import HttpRequest @@ -8,6 +9,7 @@ from realms.backends.views import finalize_session from realms.models import RealmAuthenticationSession from zentral.contrib.santa.ballot_box import BallotBox +from zentral.contrib.santa.events import SantaBallotEvent, SantaTargetStateUpdateEvent from zentral.contrib.santa.models import Ballot, Target from .utils import add_file_to_test_class, force_configuration, force_enrolled_machine, force_realm, force_realm_user @@ -20,6 +22,7 @@ def setUpTestData(cls): cls.configuration = force_configuration( voting_realm=cls.realm, default_ballot_target_types=[Target.Type.METABUNDLE, Target.Type.SIGNING_ID], + default_voting_weight=1, ) cls.em = force_enrolled_machine( configuration=cls.configuration, @@ -227,14 +230,17 @@ def test_target_get_bundle_metabundle_ballot_box(self): self.assertEqual(ballot_box.target.type, Target.Type.METABUNDLE) self.assertEqual(ballot_box.target.identifier, self.metabundle_sha256) - def test_target_post_bundle_metabundle_ballot_box_yes(self): + @patch("zentral.core.queues.backends.kombu.EventQueues.post_event") + def test_target_post_bundle_metabundle_ballot_box_yes(self, post_event): self._login() - response = self.client.post( - reverse("realms_public:santa_up:target", - args=(self.realm.pk, "bundle", self.bundle_sha256)), - {"yes_vote": "oui"}, - follow=True - ) + with self.captureOnCommitCallbacks(execute=True) as callbacks: + response = self.client.post( + reverse("realms_public:santa_up:target", + args=(self.realm.pk, "bundle", self.bundle_sha256)), + {"yes_vote": "oui"}, + follow=True + ) + self.assertEqual(len(callbacks), 1) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "user_portal/santa_target_detail.html") self.assertContains(response, "Your ballot has been cast") @@ -244,6 +250,42 @@ def test_target_post_bundle_metabundle_ballot_box_yes(self): response.context["existing_votes"], [(self.configuration, True)], ) + self.assertEqual(len(post_event.call_args_list), 2) + event1 = post_event.call_args_list[0].args[0] + self.assertIsInstance(event1, SantaBallotEvent) + self.assertEqual(len(event1.payload["votes"]), 1) + self.assertEqual(event1.payload["votes"][0]["weight"], 1) + self.assertEqual( + event1.metadata.serialize()["objects"], + {"metabundle": [f'sha256|{self.metabundle_sha256}'], + "realm_user": [str(self.realm_user.pk)], + "santa_configuration": [str(self.configuration.pk)]} + ) + event2 = post_event.call_args_list[1].args[0] + self.assertIsInstance(event2, SantaTargetStateUpdateEvent) + event2.payload.pop("created_at") + event2.payload.pop("updated_at") + self.assertEqual( + event2.payload, + {'configuration': {'name': self.configuration.name, 'pk': self.configuration.pk}, + 'new_value': {'flagged': False, + 'reset_at': None, + 'score': 1, + 'state': 0, + 'state_display': 'UNTRUSTED'}, + 'prev_value': {'flagged': False, + 'reset_at': None, + 'score': 0, + 'state': 0, + 'state_display': 'UNTRUSTED'}, + 'target': {'sha256': self.metabundle_sha256, + 'type': 'METABUNDLE'}} + ) + self.assertEqual( + event2.metadata.serialize()["objects"], + {"metabundle": [f'sha256|{self.metabundle_sha256}'], + "santa_configuration": [str(self.configuration.pk)]} + ) def test_target_post_bundle_metabundle_ballot_box_yolo(self): self._login() diff --git a/tests/server_realms/test_realm_models.py b/tests/server_realms/test_realm_models.py index 483662720f..804811f80b 100644 --- a/tests/server_realms/test_realm_models.py +++ b/tests/server_realms/test_realm_models.py @@ -1,5 +1,5 @@ from django.test import TestCase -from .utils import force_realm, force_realm_group +from .utils import force_realm, force_realm_group, force_realm_user class RealmModelsTestCase(TestCase): @@ -64,3 +64,26 @@ def test_realm_group_serialize_for_event(self): 'scim_external_id': None, 'updated_at': realm_group.updated_at} ) + + def test_realm_user_serialize_for_event_keys_only(self): + realm, realm_user = force_realm_user() + self.assertEqual( + realm_user.serialize_for_event(keys_only=True), + {'pk': str(realm_user.pk), + 'realm': {'name': realm.name, + 'pk': str(realm.pk)}, + 'username': realm_user.username} + ) + + def test_realm_user_serialize_for_event(self): + realm, realm_user = force_realm_user() + self.assertEqual( + realm_user.serialize_for_event(), + {'pk': str(realm_user.pk), + 'realm': {'name': realm.name, + 'pk': str(realm.pk)}, + 'username': realm_user.username, + 'email': realm_user.email, + 'first_name': realm_user.first_name, + 'last_name': realm_user.last_name} + ) diff --git a/zentral/contrib/santa/ballot_box.py b/zentral/contrib/santa/ballot_box.py index 5129e3f6df..b1e43e1472 100644 --- a/zentral/contrib/santa/ballot_box.py +++ b/zentral/contrib/santa/ballot_box.py @@ -1,8 +1,11 @@ from datetime import datetime import logging +import uuid from django.db import connection from django.db.models import Q from django.utils.functional import cached_property +from zentral.core.events.base import EventMetadata, EventRequest +from .events import SantaBallotEvent, SantaTargetStateUpdateEvent from .models import Ballot, Configuration, EnrolledMachine, Rule, Target, TargetState, Vote, VotingGroup from .utils import target_related_targets @@ -148,6 +151,7 @@ def __init__(self, target, voter, lock_target=True): self.target = target self.voter = voter self._set_target_states() + self._events = [] def _set_target_states(self): self.target_states = {} @@ -361,6 +365,7 @@ def _create_or_update_ballot(self, votes, event_target): was_yes_vote=yes_vote, weight=self.voter.voting_weight(configuration) ) + self._events.append((SantaBallotEvent, ballot.serialize_for_event())) return ballot def _update_target_states(self, votes): @@ -380,8 +385,22 @@ def _update_target_states(self, votes): new_score = result[0] or 0 self._update_target_state(configuration, new_score, was_yes_vote) + def _queue_target_state_update_event(self, pre_update_state, target_state): + prev_value = {} + new_value = {} + post_update_state = target_state.serialize_for_event() + for attr in ("flagged", "reset_at", "score", "state", "state_display"): + prev_value[attr] = pre_update_state.get(attr) + new_value[attr] = post_update_state.pop(attr) + if prev_value != new_value: + event_payload = post_update_state + event_payload["new_value"] = new_value + event_payload["prev_value"] = prev_value + self._events.append((SantaTargetStateUpdateEvent, event_payload)) + def _update_target_state(self, configuration, score, was_yes_vote): target_state = self.target_states[configuration] + pre_update_state = target_state.serialize_for_event() if was_yes_vote: if target_state.flagged and self.voter.can_unflag_target(configuration): target_state.flagged = False @@ -394,6 +413,7 @@ def _update_target_state(self, configuration, score, was_yes_vote): target_state.state = TargetState.State.SUSPECT target_state.score = score target_state.save() + self._queue_target_state_update_event(pre_update_state, target_state) def _update_target_state_state(self, target_state, score): configuration = target_state.configuration @@ -423,11 +443,13 @@ def reset_target_state(self, configuration): if not self.voter.can_reset_target(configuration): raise ResetNotAllowedError target_state = self.target_states[configuration] + pre_update_state = target_state.serialize_for_event() target_state.score = 0 target_state.flagged = False self._update_target_state_state(target_state, 0) target_state.reset_at = datetime.utcnow() target_state.save() + self._queue_target_state_update_event(pre_update_state, target_state) # rules @@ -481,3 +503,17 @@ def _blocklist(self, configuration): def _ensure_no_rules(self, configuration): Rule.objects.filter(configuration=configuration, target__in=self._iter_rule_targets()).delete() + + # events + + def post_events(self, request, machine_serial_number=None): + event_request = EventRequest.build_from_request(request) + event_uuid = uuid.uuid4() + for index, (event_class, event_payload) in enumerate(self._events): + event_metadata = EventMetadata( + uuid=event_uuid, index=index, + request=event_request, + machine_serial_number=machine_serial_number, + ) + event = event_class(event_metadata, event_payload) + event.post() diff --git a/zentral/contrib/santa/events/__init__.py b/zentral/contrib/santa/events/__init__.py index 682f0cce7c..b341cfe45c 100644 --- a/zentral/contrib/santa/events/__init__.py +++ b/zentral/contrib/santa/events/__init__.py @@ -154,59 +154,100 @@ def get_linked_objects_keys(self): register_event_type(SantaRuleSetUpdateEvent) -class SantaRuleUpdateEvent(BaseEvent): - event_type = "santa_rule_update" - tags = ["santa"] - - def get_linked_objects_keys(self): - keys = {} - rule = self.payload.get("rule") - if not rule: - return keys - configuration = rule.get("configuration") - if configuration: - keys["santa_configuration"] = [(configuration.get("pk"),)] - ruleset = rule.get("ruleset") - if ruleset: - keys["santa_ruleset"] = [(ruleset.get("pk"),)] - target = rule.get("target") - if not target: - return keys +class TargetEventMixin: + def add_target_to_linked_objects_keys(self, keys, attr="target"): + target = self.payload + for payload_key in attr.split("."): + target = target.get(payload_key) + if not target: + return target_type = target.get("type") if not target_type: - return keys + return try: target_type = Target.Type(target_type) except (ValueError, TypeError): logger.error("Invalid target type") - return keys + return if target_type == Target.Type.CDHASH: cdhash = target.get("cdhash") if cdhash: - keys["file"] = [("cdhash", cdhash)] + keys.setdefault("file", []).append(("cdhash", cdhash)) elif target_type == Target.Type.SIGNING_ID: signing_id = target.get("signing_id") if signing_id: - keys["file"] = [("apple_signing_id", signing_id)] + keys.setdefault("file", []).append(("apple_signing_id", signing_id)) elif target_type == Target.Type.TEAM_ID: team_id = target.get("team_id") if team_id: - keys["apple_team_id"] = [(team_id,)] + keys.setdefault("apple_team_id", []).append((team_id,)) else: sha256 = target.get("sha256") if sha256: if target_type == Target.Type.BINARY: - keys["file"] = [("sha256", sha256)] - elif target_type == Target.Type.CERTIFICATE: - keys["certificate"] = [("sha256", sha256)] - elif target_type == Target.Type.BUNDLE: - keys["bundle"] = [("sha256", sha256)] + key_attr = "file" + else: + key_attr = target_type.name.lower() + keys.setdefault(key_attr, []).append(("sha256", sha256)) + + +class SantaRuleUpdateEvent(TargetEventMixin, BaseEvent): + event_type = "santa_rule_update" + tags = ["santa"] + + def get_linked_objects_keys(self): + keys = {} + self.add_target_to_linked_objects_keys(keys, attr="rule.target") + rule = self.payload.get("rule") + if not rule: + return keys + configuration = rule.get("configuration") + if configuration: + keys["santa_configuration"] = [(configuration.get("pk"),)] + ruleset = rule.get("ruleset") + if ruleset: + keys["santa_ruleset"] = [(ruleset.get("pk"),)] return keys register_event_type(SantaRuleUpdateEvent) +class SantaBallotEvent(TargetEventMixin, BaseEvent): + event_type = "santa_ballot" + tags = ["santa"] + + def get_linked_objects_keys(self): + keys = {} + for attr in ("target", "event_target"): + self.add_target_to_linked_objects_keys(keys, attr) + realm_user = self.payload.get("realm_user") + if realm_user: + keys["realm_user"] = [(realm_user["pk"],)] + for vote in self.payload.get("votes", []): + keys["santa_configuration"] = [(vote["configuration"]["pk"],)] + return keys + + +register_event_type(SantaBallotEvent) + + +class SantaTargetStateUpdateEvent(TargetEventMixin, BaseEvent): + event_type = "santa_target_state_update" + tags = ["santa"] + + def get_linked_objects_keys(self): + keys = {} + self.add_target_to_linked_objects_keys(keys) + configuration = self.payload.get("configuration") + if configuration: + keys["santa_configuration"] = [(configuration["pk"],)] + return keys + + +register_event_type(SantaTargetStateUpdateEvent) + + def _build_certificate_tree_from_santa_event_cert(in_d): out_d = {} for from_a, to_a, is_dt in (("cn", "common_name", False), diff --git a/zentral/contrib/santa/models.py b/zentral/contrib/santa/models.py index 35e7c3fb86..bbc11f7fe9 100644 --- a/zentral/contrib/santa/models.py +++ b/zentral/contrib/santa/models.py @@ -336,6 +336,20 @@ class State(models.IntegerChoices): class Meta: unique_together = (("target", "configuration"),) + def serialize_for_event(self): + state = self.State(self.state) + return { + "target": self.target.serialize_for_event(), + "configuration": self.configuration.serialize_for_event(keys_only=True), + "flagged": self.flagged, + "state": state.value, + "state_display": state.name, + "score": self.score, + "reset_at": self.reset_at, + "created_at": self.created_at, + "updated_at": self.updated_at, + } + class MetaBundle(models.Model): target = models.OneToOneField(Target, on_delete=models.PROTECT) @@ -824,6 +838,26 @@ class Ballot(models.Model): class Meta: unique_together = (("target", "realm_user", "user_uid", "replaced_by"),) + def serialize_for_event(self, keys_only=False): + d = { + "pk": str(self.id), + } + if keys_only: + return d + d.update({ + "target": self.target.serialize_for_event(), + "event_target": self.event_target.serialize_for_event() if self.event_target else None, + "realm_user": self.realm_user.serialize_for_event(keys_only=True), + "user_uid": self.user_uid, + "replaced_by": self.replaced_by.serialize_for_event(keys_only=True) if self.replaced_by else None, + "votes": [ + vote.serialize_for_event() + for vote in self.vote_set.all() + ], + "created_at": self.created_at, + }) + return d + class Vote(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -836,6 +870,15 @@ class Vote(models.Model): class Meta: unique_together = (("ballot", "configuration"),) + def serialize_for_event(self): + return { + "pk": str(self.id), + "configuration": self.configuration.serialize_for_event(keys_only=True), + "was_yes_vote": self.was_yes_vote, + "weight": self.weight, + "created_at": self.created_at, + } + class RuleSet(models.Model): name = models.CharField(max_length=256, unique=True) diff --git a/zentral/contrib/santa/up_views.py b/zentral/contrib/santa/up_views.py index 5f967d7997..ae63b29e9b 100644 --- a/zentral/contrib/santa/up_views.py +++ b/zentral/contrib/santa/up_views.py @@ -2,6 +2,7 @@ from uuid import UUID from django.contrib import messages from django.core.exceptions import SuspiciousOperation +from django.db import transaction from django.shortcuts import get_object_or_404, redirect from django.views.generic import View from realms.up_views import UPLoginRequiredMixin, UPTemplateView @@ -66,20 +67,28 @@ def dispatch(self, request, *args, **kwargs): self.current_machine_id = None return super().dispatch(request, *args, **kwargs) + def get_ballot_box_and_machines(self): + self.ballot_box = BallotBox.for_realm_user(self.target, self.realm_user, lock_target=False) + self.machines = [] + self.current_machine = None + self.current_configuration = None + for em, last_seen in self.ballot_box.voter.enrolled_machines: + mm = MetaMachine(em.serial_number) + if em.hardware_uuid == self.current_machine_id: + self.current_machine = mm + self.current_configuration = em.enrollment.configuration + self.machines.append(mm) + def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx["machines"] = [] ctx["target"] = self.target - ballot_box = BallotBox.for_realm_user(self.target, self.realm_user, lock_target=False) - for em, last_seen in ballot_box.voter.enrolled_machines: - mm = MetaMachine(em.serial_number) - if em.hardware_uuid == self.current_machine_id: - ctx["current_machine"] = mm - ctx["current_configuration"] = em.enrollment.configuration - ctx["machines"].append(mm) - ctx["target_info"] = ballot_box.target_info() - ctx["publisher_info"] = ballot_box.publisher_info() - ctx["ballot_box"] = ballot_box.best_ballot_box() + self.get_ballot_box_and_machines() + ctx["machines"] = self.machines + ctx["current_machine"] = self.current_machine + ctx["current_configuration"] = self.current_configuration + ctx["target_info"] = self.ballot_box.target_info() + ctx["publisher_info"] = self.ballot_box.publisher_info() + ctx["ballot_box"] = self.ballot_box.best_ballot_box() if ctx["ballot_box"]: ctx["states"] = sorted( ctx["ballot_box"].target_states.items(), @@ -107,13 +116,23 @@ def post(self, request, *args, **kwargs): except Exception: logger.error("Could not find event target in DB") event_target = None - ballot_box = BallotBox.for_realm_user(self.target, self.realm_user, lock_target=False) + + self.get_ballot_box_and_machines() try: - ballot_box.best_ballot_box(lock_target=True).cast_default_votes(yes_vote == "oui", event_target) + best_ballot_box = self.ballot_box.best_ballot_box(lock_target=True) + best_ballot_box.cast_default_votes(yes_vote == "oui", event_target) except DuplicateVoteError: messages.error(request, "You cannot cast the same ballot twice") else: messages.info(request, "Your ballot has been cast") + + def on_commit_callback(): + best_ballot_box.post_events( + self.request, + self.current_machine.serial_number if self.current_machine else None, + ) + + transaction.on_commit(on_commit_callback) return redirect("realms_public:santa_up:target", realm_pk=self.realm.pk, type=Target.Type(self.target.type).value.lower(), diff --git a/zentral/contrib/santa/views/ballots.py b/zentral/contrib/santa/views/ballots.py index 315440d9e5..0004b01ff1 100644 --- a/zentral/contrib/santa/views/ballots.py +++ b/zentral/contrib/santa/views/ballots.py @@ -3,6 +3,7 @@ from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import PermissionDenied +from django.db import transaction from django.shortcuts import get_object_or_404, redirect from django.views.generic import TemplateView from zentral.utils.views import UserPaginationMixin @@ -135,6 +136,12 @@ def post(self, request, *args, **kwargs): messages.error(request, "The ballot was rejected") else: messages.info(request, "Your ballot has been cast") + + def on_commit_callback(): + ballot_box.post_events(self.request) + + transaction.on_commit(on_commit_callback) + return redirect(self.target) else: messages.error(request, "Invalid ballot") diff --git a/zentral/contrib/santa/views/targets.py b/zentral/contrib/santa/views/targets.py index 28e05eb824..e0b0fcc5c5 100644 --- a/zentral/contrib/santa/views/targets.py +++ b/zentral/contrib/santa/views/targets.py @@ -3,6 +3,7 @@ from urllib.parse import urlencode from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin +from django.db import transaction from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.views.generic import TemplateView @@ -482,4 +483,10 @@ def post(self, request, *args, **kwargs): messages.error(request, "Target state reset not allowed") else: messages.info(request, "Target state reset") + + def on_commit_callback(): + self.ballot_box.post_events(self.request) + + transaction.on_commit(on_commit_callback) + return redirect(self.target)