events: add local transport mode (#2992)

* events: add local transport mode

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add default local transport

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2022-05-30 20:55:05 +02:00 committed by GitHub
parent fc75867218
commit 8faa1bf865
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 811 additions and 824 deletions

View file

@ -0,0 +1,49 @@
# Generated by Django 4.0.4 on 2022-05-30 18:08
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.events.models import TransportMode
def notify_local_transport(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
NotificationRule = apps.get_model("authentik_events", "NotificationRule")
local_transport, _ = NotificationTransport.objects.using(db_alias).update_or_create(
name="default-local-transport",
defaults={"mode": TransportMode.LOCAL},
)
for trigger in NotificationRule.objects.using(db_alias).filter(
name__in=[
"default-notify-configuration-error",
"default-notify-exception",
"default-notify-update",
]
):
trigger.transports.add(local_transport)
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0001_squashed_0019_alter_notificationtransport_webhook_url"),
]
operations = [
migrations.AlterField(
model_name="notificationtransport",
name="mode",
field=models.TextField(
choices=[
("local", "authentik inbuilt notifications"),
("webhook", "Generic Webhook"),
("webhook_slack", "Slack Webhook (Slack/Discord)"),
("email", "Email"),
]
),
),
migrations.RunPython(notify_local_transport),
]

View file

@ -289,6 +289,7 @@ class Event(ExpiringModel):
class TransportMode(models.TextChoices): class TransportMode(models.TextChoices):
"""Modes that a notification transport can send a notification""" """Modes that a notification transport can send a notification"""
LOCAL = "local", _("authentik inbuilt notifications")
WEBHOOK = "webhook", _("Generic Webhook") WEBHOOK = "webhook", _("Generic Webhook")
WEBHOOK_SLACK = "webhook_slack", _("Slack Webhook (Slack/Discord)") WEBHOOK_SLACK = "webhook_slack", _("Slack Webhook (Slack/Discord)")
EMAIL = "email", _("Email") EMAIL = "email", _("Email")
@ -315,6 +316,8 @@ class NotificationTransport(models.Model):
def send(self, notification: "Notification") -> list[str]: def send(self, notification: "Notification") -> list[str]:
"""Send notification to user, called from async task""" """Send notification to user, called from async task"""
if self.mode == TransportMode.LOCAL:
return self.send_local(notification)
if self.mode == TransportMode.WEBHOOK: if self.mode == TransportMode.WEBHOOK:
return self.send_webhook(notification) return self.send_webhook(notification)
if self.mode == TransportMode.WEBHOOK_SLACK: if self.mode == TransportMode.WEBHOOK_SLACK:
@ -323,6 +326,17 @@ class NotificationTransport(models.Model):
return self.send_email(notification) return self.send_email(notification)
raise ValueError(f"Invalid mode {self.mode} set") raise ValueError(f"Invalid mode {self.mode} set")
def send_local(self, notification: "Notification") -> list[str]:
"""Local notification delivery"""
if self.webhook_mapping:
self.webhook_mapping.evaluate(
user=notification.user,
request=None,
notification=notification,
)
notification.save()
return []
def send_webhook(self, notification: "Notification") -> list[str]: def send_webhook(self, notification: "Notification") -> list[str]:
"""Send notification to generic webhook""" """Send notification to generic webhook"""
default_body = { default_body = {

View file

@ -1,8 +1,11 @@
"""Event notification tasks""" """Event notification tasks"""
from typing import Optional
from django.db.models.query_utils import Q from django.db.models.query_utils import Q
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import ( from authentik.events.models import (
Event, Event,
@ -39,10 +42,9 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid) LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid)
return return
event: Event = events.first() event: Event = events.first()
triggers: NotificationRule = NotificationRule.objects.filter(name=trigger_name) trigger: Optional[NotificationRule] = NotificationRule.objects.filter(name=trigger_name).first()
if not triggers.exists(): if not trigger:
return return
trigger = triggers.first()
if "policy_uuid" in event.context: if "policy_uuid" in event.context:
policy_uuid = event.context["policy_uuid"] policy_uuid = event.context["policy_uuid"]
@ -81,11 +83,14 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
for transport in trigger.transports.all(): for transport in trigger.transports.all():
for user in trigger.group.users.all(): for user in trigger.group.users.all():
LOGGER.debug("created notification") LOGGER.debug("created notification")
notification = Notification.objects.create(
severity=trigger.severity, body=event.summary, event=event, user=user
)
notification_transport.apply_async( notification_transport.apply_async(
args=[notification.pk, transport.pk], queue="authentik_events" args=[
transport.pk,
str(event.pk),
user.pk,
str(trigger.pk),
],
queue="authentik_events",
) )
if transport.send_once: if transport.send_once:
break break
@ -97,19 +102,30 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
retry_backoff=True, retry_backoff=True,
base=MonitoredTask, base=MonitoredTask,
) )
def notification_transport(self: MonitoredTask, notification_pk: int, transport_pk: int): def notification_transport(
self: MonitoredTask, transport_pk: int, event_pk: str, user_pk: int, trigger_pk: str
):
"""Send notification over specified transport""" """Send notification over specified transport"""
self.save_on_success = False self.save_on_success = False
try: try:
notification: Notification = Notification.objects.filter(pk=notification_pk).first() event = Event.objects.filter(pk=event_pk).first()
if not notification: if not event:
return return
user = User.objects.filter(pk=user_pk).first()
if not user:
return
trigger = NotificationRule.objects.filter(pk=trigger_pk).first()
if not trigger:
return
notification = Notification(
severity=trigger.severity, body=event.summary, event=event, user=user
)
transport = NotificationTransport.objects.filter(pk=transport_pk).first() transport = NotificationTransport.objects.filter(pk=transport_pk).first()
if not transport: if not transport:
return return
transport.send(notification) transport.send(notification)
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL)) self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
except NotificationTransportError as exc: except (NotificationTransportError, PropertyMappingExpressionException) as exc:
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
raise exc raise exc

View file

@ -11,7 +11,10 @@ from authentik.events.models import (
Notification, Notification,
NotificationRule, NotificationRule,
NotificationTransport, NotificationTransport,
NotificationWebhookMapping,
TransportMode,
) )
from authentik.lib.generators import generate_id
from authentik.policies.event_matcher.models import EventMatcherPolicy from authentik.policies.event_matcher.models import EventMatcherPolicy
from authentik.policies.exceptions import PolicyException from authentik.policies.exceptions import PolicyException
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
@ -105,4 +108,26 @@ class TestEventsNotifications(TestCase):
execute_mock = MagicMock() execute_mock = MagicMock()
with patch("authentik.events.models.NotificationTransport.send", execute_mock): with patch("authentik.events.models.NotificationTransport.send", execute_mock):
Event.new(EventAction.CUSTOM_PREFIX).save() Event.new(EventAction.CUSTOM_PREFIX).save()
self.assertEqual(Notification.objects.count(), 1) self.assertEqual(execute_mock.call_count, 1)
def test_transport_mapping(self):
"""Test transport mapping"""
mapping = NotificationWebhookMapping.objects.create(
name=generate_id(),
expression="""notification.body = 'foo'""",
)
transport = NotificationTransport.objects.create(
name="transport", webhook_mapping=mapping, mode=TransportMode.LOCAL
)
NotificationRule.objects.filter(name__startswith="default").delete()
trigger = NotificationRule.objects.create(name="trigger", group=self.group)
trigger.transports.add(transport)
matcher = EventMatcherPolicy.objects.create(
name="matcher", action=EventAction.CUSTOM_PREFIX
)
PolicyBinding.objects.create(target=trigger, policy=matcher, order=0)
Notification.objects.all().delete()
Event.new(EventAction.CUSTOM_PREFIX).save()
self.assertEqual(Notification.objects.first().body, "foo")

View file

@ -4490,6 +4490,7 @@ paths:
type: string type: string
enum: enum:
- email - email
- local
- webhook - webhook
- webhook_slack - webhook_slack
- in: query - in: query
@ -23047,6 +23048,7 @@ components:
- pk - pk
NotificationTransportModeEnum: NotificationTransportModeEnum:
enum: enum:
- local
- webhook - webhook
- webhook_slack - webhook_slack
- email - email

View file

@ -2973,6 +2973,10 @@ msgstr "Lädt..."
msgid "Local" msgid "Local"
msgstr "Lokal" msgstr "Lokal"
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr ""
#: src/user/user-settings/details/UserDetailsForm.ts #: src/user/user-settings/details/UserDetailsForm.ts
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "Gebietsschema" #~ msgstr "Gebietsschema"

View file

@ -3025,6 +3025,10 @@ msgstr "Loading..."
msgid "Local" msgid "Local"
msgstr "Local" msgstr "Local"
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr "Local (notifications will be created within authentik)"
#: src/user/user-settings/details/UserDetailsForm.ts #: src/user/user-settings/details/UserDetailsForm.ts
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "Locale" #~ msgstr "Locale"

View file

@ -2966,6 +2966,10 @@ msgstr "Cargando..."
msgid "Local" msgid "Local"
msgstr "Local" msgstr "Local"
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr ""
#: src/user/user-settings/details/UserDetailsForm.ts #: src/user/user-settings/details/UserDetailsForm.ts
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "Lugar" #~ msgstr "Lugar"

View file

@ -2997,6 +2997,10 @@ msgstr "Chargement en cours..."
msgid "Local" msgid "Local"
msgstr "Local" msgstr "Local"
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr ""
#: src/user/user-settings/details/UserDetailsForm.ts #: src/user/user-settings/details/UserDetailsForm.ts
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "" #~ msgstr ""

View file

@ -2963,6 +2963,10 @@ msgstr "Ładowanie..."
msgid "Local" msgid "Local"
msgstr "Lokalny" msgstr "Lokalny"
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr ""
#: src/user/user-settings/details/UserDetailsForm.ts #: src/user/user-settings/details/UserDetailsForm.ts
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "Język" #~ msgstr "Język"

View file

@ -3007,6 +3007,10 @@ msgstr ""
msgid "Local" msgid "Local"
msgstr "" msgstr ""
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr ""
#: src/user/user-settings/details/UserDetailsForm.ts #: src/user/user-settings/details/UserDetailsForm.ts
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "" #~ msgstr ""

View file

@ -2967,6 +2967,10 @@ msgstr "Yükleniyor..."
msgid "Local" msgid "Local"
msgstr "Yerel" msgstr "Yerel"
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr ""
#: src/user/user-settings/details/UserDetailsForm.ts #: src/user/user-settings/details/UserDetailsForm.ts
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "Yerelleştirme" #~ msgstr "Yerelleştirme"

File diff suppressed because it is too large Load diff

View file

@ -2953,6 +2953,10 @@ msgstr "载入中……"
msgid "Local" msgid "Local"
msgstr "本地" msgstr "本地"
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr ""
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "区域设置" #~ msgstr "区域设置"

View file

@ -2953,6 +2953,10 @@ msgstr "载入中……"
msgid "Local" msgid "Local"
msgstr "本地" msgstr "本地"
#: src/pages/events/TransportForm.ts
msgid "Local (notifications will be created within authentik)"
msgstr ""
#~ msgid "Locale" #~ msgid "Locale"
#~ msgstr "区域设置" #~ msgstr "区域设置"

View file

@ -56,6 +56,12 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
renderTransportModes(): TemplateResult { renderTransportModes(): TemplateResult {
return html` return html`
<option
value=${NotificationTransportModeEnum.Local}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Local}
>
${t`Local (notifications will be created within authentik)`}
</option>
<option <option
value=${NotificationTransportModeEnum.Email} value=${NotificationTransportModeEnum.Email}
?selected=${this.instance?.mode === NotificationTransportModeEnum.Email} ?selected=${this.instance?.mode === NotificationTransportModeEnum.Email}