diff --git a/authentik/events/forms.py b/authentik/events/forms.py index 7243e01c9..6b2e7acba 100644 --- a/authentik/events/forms.py +++ b/authentik/events/forms.py @@ -15,6 +15,7 @@ class NotificationTransportForm(forms.ModelForm): "name", "mode", "webhook_url", + "send_once", ] widgets = { "name": forms.TextInput(), diff --git a/authentik/events/migrations/0012_auto_20210202_1821.py b/authentik/events/migrations/0012_auto_20210202_1821.py new file mode 100644 index 000000000..36aa9d14e --- /dev/null +++ b/authentik/events/migrations/0012_auto_20210202_1821.py @@ -0,0 +1,52 @@ +# Generated by Django 3.1.6 on 2021-02-02 18:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_events", "0011_notification_rules_default_v1"), + ] + + operations = [ + migrations.AddField( + model_name="notificationtransport", + name="send_once", + field=models.BooleanField( + default=False, + help_text="Only send notification once, for example when sending a webhook into a chat channel.", + ), + ), + migrations.AlterField( + model_name="event", + name="action", + field=models.TextField( + choices=[ + ("login", "Login"), + ("login_failed", "Login Failed"), + ("logout", "Logout"), + ("user_write", "User Write"), + ("suspicious_request", "Suspicious Request"), + ("password_set", "Password Set"), + ("token_view", "Token View"), + ("invitation_used", "Invite Used"), + ("authorize_application", "Authorize Application"), + ("source_linked", "Source Linked"), + ("impersonation_started", "Impersonation Started"), + ("impersonation_ended", "Impersonation Ended"), + ("policy_execution", "Policy Execution"), + ("policy_exception", "Policy Exception"), + ("property_mapping_exception", "Property Mapping Exception"), + ("system_task_execution", "System Task Execution"), + ("system_task_exception", "System Task Exception"), + ("configuration_error", "Configuration Error"), + ("model_created", "Model Created"), + ("model_updated", "Model Updated"), + ("model_deleted", "Model Deleted"), + ("update_available", "Update Available"), + ("custom_", "Custom Prefix"), + ] + ), + ), + ] diff --git a/authentik/events/models.py b/authentik/events/models.py index 4e7d933c9..81e3a8b25 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -184,6 +184,12 @@ class NotificationTransport(models.Model): mode = models.TextField(choices=TransportMode.choices) webhook_url = models.TextField(blank=True) + send_once = models.BooleanField( + default=False, + help_text=_( + "Only send notification once, for example when sending a webhook into a chat channel." + ), + ) def send(self, notification: "Notification") -> list[str]: """Send notification to user, called from async task""" diff --git a/authentik/events/tasks.py b/authentik/events/tasks.py index 1f30f73a3..43505b8e5 100644 --- a/authentik/events/tasks.py +++ b/authentik/events/tasks.py @@ -65,15 +65,17 @@ def event_trigger_handler(event_uuid: str, trigger_name: str): LOGGER.debug("e(trigger): event trigger matched", trigger=trigger) # Create the notification objects - for user in trigger.group.users.all(): - notification = Notification.objects.create( - severity=trigger.severity, body=event.summary, event=event, user=user - ) - - for transport in trigger.transports.all(): + for transport in trigger.transports.all(): + for user in trigger.group.users.all(): + LOGGER.debug("created notif") + notification = Notification.objects.create( + severity=trigger.severity, body=event.summary, event=event, user=user + ) notification_transport.apply_async( args=[notification.pk, transport.pk], queue="authentik_events" ) + if transport.send_once: + break @CELERY_APP.task( diff --git a/authentik/events/tests/test_notifications.py b/authentik/events/tests/test_notifications.py index 98c23b5fb..ae80c0ea1 100644 --- a/authentik/events/tests/test_notifications.py +++ b/authentik/events/tests/test_notifications.py @@ -8,6 +8,7 @@ from authentik.core.models import Group, User from authentik.events.models import ( Event, EventAction, + Notification, NotificationRule, NotificationTransport, ) @@ -21,7 +22,7 @@ class TestEventsNotifications(TestCase): def setUp(self) -> None: self.group = Group.objects.create(name="test-group") - self.user = User.objects.create(name="test-user") + self.user = User.objects.create(name="test-user", username="test") self.group.users.add(self.user) self.group.save() @@ -88,3 +89,26 @@ class TestEventsNotifications(TestCase): ): Event.new(EventAction.CUSTOM_PREFIX).save() self.assertEqual(passes.call_count, 1) + + def test_transport_once(self): + """Test transport's send_once""" + user2 = User.objects.create(name="test2-user", username="test2") + self.group.users.add(user2) + self.group.save() + + transport = NotificationTransport.objects.create( + name="transport", send_once=True + ) + NotificationRule.objects.filter(name__startswith="default").delete() + trigger = NotificationRule.objects.create(name="trigger", group=self.group) + trigger.transports.add(transport) + trigger.save() + matcher = EventMatcherPolicy.objects.create( + name="matcher", action=EventAction.CUSTOM_PREFIX + ) + PolicyBinding.objects.create(target=trigger, policy=matcher, order=0) + + execute_mock = MagicMock() + with patch("authentik.events.models.NotificationTransport.send", execute_mock): + Event.new(EventAction.CUSTOM_PREFIX).save() + self.assertEqual(Notification.objects.count(), 1) diff --git a/authentik/policies/event_matcher/migrations/0005_auto_20210202_1821.py b/authentik/policies/event_matcher/migrations/0005_auto_20210202_1821.py new file mode 100644 index 000000000..4e0b80b8d --- /dev/null +++ b/authentik/policies/event_matcher/migrations/0005_auto_20210202_1821.py @@ -0,0 +1,46 @@ +# Generated by Django 3.1.6 on 2021-02-02 18:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_policies_event_matcher", "0004_auto_20210112_2158"), + ] + + operations = [ + migrations.AlterField( + model_name="eventmatcherpolicy", + name="action", + field=models.TextField( + blank=True, + choices=[ + ("login", "Login"), + ("login_failed", "Login Failed"), + ("logout", "Logout"), + ("user_write", "User Write"), + ("suspicious_request", "Suspicious Request"), + ("password_set", "Password Set"), + ("token_view", "Token View"), + ("invitation_used", "Invite Used"), + ("authorize_application", "Authorize Application"), + ("source_linked", "Source Linked"), + ("impersonation_started", "Impersonation Started"), + ("impersonation_ended", "Impersonation Ended"), + ("policy_execution", "Policy Execution"), + ("policy_exception", "Policy Exception"), + ("property_mapping_exception", "Property Mapping Exception"), + ("system_task_execution", "System Task Execution"), + ("system_task_exception", "System Task Exception"), + ("configuration_error", "Configuration Error"), + ("model_created", "Model Created"), + ("model_updated", "Model Updated"), + ("model_deleted", "Model Deleted"), + ("update_available", "Update Available"), + ("custom_", "Custom Prefix"), + ], + help_text="Match created events with this action type. When left empty, all action types will be matched.", + ), + ), + ] diff --git a/swagger.yaml b/swagger.yaml index bd3b20848..3f042f94d 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -7701,6 +7701,11 @@ definitions: webhook_url: title: Webhook url type: string + send_once: + title: Send once + description: Only send notification once, for example when sending a + webhook into a chat channel. + type: boolean readOnly: true severity: title: Severity