Merge branch 'main' into celery-2-dramatiq

This commit is contained in:
Marc 'risson' Schmitt
2025-06-18 14:42:24 +02:00
18 changed files with 682 additions and 49 deletions

View File

@ -138,6 +138,7 @@ class EventViewSet(ModelViewSet):
from authentik.enterprise.search.fields import ChoiceSearchField, JSONSearchField
return [
StrField(Event, "event_uuid"),
StrField(Event, "app", suggest_options=True),
StrField(Event, "client_ip"),
JSONSearchField(Event, "user"),

View File

@ -11,7 +11,7 @@ from authentik.events.models import NotificationRule
class NotificationRuleSerializer(ModelSerializer):
"""NotificationRule Serializer"""
group_obj = GroupSerializer(read_only=True, source="group")
destination_group_obj = GroupSerializer(read_only=True, source="destination_group")
class Meta:
model = NotificationRule
@ -20,8 +20,9 @@ class NotificationRuleSerializer(ModelSerializer):
"name",
"transports",
"severity",
"group",
"group_obj",
"destination_group",
"destination_group_obj",
"destination_event_user",
]
@ -30,6 +31,6 @@ class NotificationRuleViewSet(UsedByMixin, ModelViewSet):
queryset = NotificationRule.objects.all()
serializer_class = NotificationRuleSerializer
filterset_fields = ["name", "severity", "group__name"]
filterset_fields = ["name", "severity", "destination_group__name"]
ordering = ["name"]
search_fields = ["name", "group__name"]
search_fields = ["name", "destination_group__name"]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.1.11 on 2025-06-16 23:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0009_remove_notificationtransport_webhook_mapping_and_more"),
]
operations = [
migrations.RenameField(
model_name="notificationrule",
old_name="group",
new_name="destination_group",
),
migrations.AddField(
model_name="notificationrule",
name="destination_event_user",
field=models.BooleanField(
default=False,
help_text="When enabled, notification will be sent to user the user that triggered the event.When destination_group is configured, notification is sent to both.",
),
),
]

View File

@ -1,10 +1,12 @@
"""authentik events models"""
from collections.abc import Generator
from datetime import timedelta
from difflib import get_close_matches
from functools import lru_cache
from inspect import currentframe
from smtplib import SMTPException
from typing import Any
from uuid import uuid4
from django.apps import apps
@ -549,7 +551,7 @@ class NotificationRule(SerializerModel, PolicyBindingModel):
default=NotificationSeverity.NOTICE,
help_text=_("Controls which severity level the created notifications will have."),
)
group = models.ForeignKey(
destination_group = models.ForeignKey(
Group,
help_text=_(
"Define which group of users this notification should be sent and shown to. "
@ -559,6 +561,19 @@ class NotificationRule(SerializerModel, PolicyBindingModel):
blank=True,
on_delete=models.SET_NULL,
)
destination_event_user = models.BooleanField(
default=False,
help_text=_(
"When enabled, notification will be sent to user the user that triggered the event."
"When destination_group is configured, notification is sent to both."
),
)
def destination_users(self, event: Event) -> Generator[User, Any]:
if self.destination_event_user and event.user.get("pk"):
yield User(pk=event.user.get("pk"))
if self.destination_group:
yield from self.destination_group.users.all()
@property
def serializer(self) -> type[Serializer]:

View File

@ -60,14 +60,10 @@ def event_trigger_handler(event_uuid: UUID, trigger_name: str):
if not result.passing:
return
if not trigger.group:
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
return
LOGGER.debug("e(trigger): event trigger matched", trigger=trigger)
# Create the notification objects
for transport in trigger.transports.all():
for user in trigger.group.users.all():
for user in trigger.destination_users(event):
LOGGER.debug("created notification")
notification_transport.send_with_options(
args=(

View File

@ -6,6 +6,7 @@ from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_user
from authentik.events.models import (
Event,
EventAction,
@ -34,7 +35,7 @@ class TestEventsNotifications(APITestCase):
def test_trigger_empty(self):
"""Test trigger without any policies attached"""
transport = NotificationTransport.objects.create(name=generate_id())
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)
trigger = NotificationRule.objects.create(name=generate_id(), destination_group=self.group)
trigger.transports.add(transport)
trigger.save()
@ -46,7 +47,7 @@ class TestEventsNotifications(APITestCase):
def test_trigger_single(self):
"""Test simple transport triggering"""
transport = NotificationTransport.objects.create(name=generate_id())
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)
trigger = NotificationRule.objects.create(name=generate_id(), destination_group=self.group)
trigger.transports.add(transport)
trigger.save()
matcher = EventMatcherPolicy.objects.create(
@ -59,6 +60,25 @@ class TestEventsNotifications(APITestCase):
Event.new(EventAction.CUSTOM_PREFIX).save()
self.assertEqual(execute_mock.call_count, 1)
def test_trigger_event_user(self):
"""Test trigger with event user"""
user = create_test_user()
transport = NotificationTransport.objects.create(name=generate_id())
trigger = NotificationRule.objects.create(name=generate_id(), destination_event_user=True)
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).set_user(user).save()
self.assertEqual(execute_mock.call_count, 1)
notification: Notification = execute_mock.call_args[0][0]
self.assertEqual(notification.user, user)
def test_trigger_no_group(self):
"""Test trigger without group"""
trigger = NotificationRule.objects.create(name=generate_id())
@ -76,7 +96,7 @@ class TestEventsNotifications(APITestCase):
"""Test Policy error which would cause recursion"""
transport = NotificationTransport.objects.create(name=generate_id())
NotificationRule.objects.filter(name__startswith="default").delete()
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)
trigger = NotificationRule.objects.create(name=generate_id(), destination_group=self.group)
trigger.transports.add(transport)
trigger.save()
matcher = EventMatcherPolicy.objects.create(
@ -99,7 +119,7 @@ class TestEventsNotifications(APITestCase):
transport = NotificationTransport.objects.create(name=generate_id(), send_once=True)
NotificationRule.objects.filter(name__startswith="default").delete()
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)
trigger = NotificationRule.objects.create(name=generate_id(), destination_group=self.group)
trigger.transports.add(transport)
trigger.save()
matcher = EventMatcherPolicy.objects.create(
@ -123,7 +143,7 @@ class TestEventsNotifications(APITestCase):
name=generate_id(), webhook_mapping_body=mapping, mode=TransportMode.LOCAL
)
NotificationRule.objects.filter(name__startswith="default").delete()
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)
trigger = NotificationRule.objects.create(name=generate_id(), destination_group=self.group)
trigger.transports.add(transport)
matcher = EventMatcherPolicy.objects.create(
name="matcher", action=EventAction.CUSTOM_PREFIX

View File

@ -6671,11 +6671,16 @@
"title": "Severity",
"description": "Controls which severity level the created notifications will have."
},
"group": {
"destination_group": {
"type": "string",
"format": "uuid",
"title": "Group",
"title": "Destination group",
"description": "Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent."
},
"destination_event_user": {
"type": "boolean",
"title": "Destination event user",
"description": "When enabled, notification will be sent to user the user that triggered the event.When destination_group is configured, notification is sent to both."
}
},
"required": []

View File

@ -7758,7 +7758,7 @@ paths:
description: NotificationRule Viewset
parameters:
- in: query
name: group__name
name: destination_group__name
schema:
type: string
- in: query
@ -49002,18 +49002,23 @@ components:
- $ref: '#/components/schemas/SeverityEnum'
description: Controls which severity level the created notifications will
have.
group:
destination_group:
type: string
format: uuid
nullable: true
description: Define which group of users this notification should be sent
and shown to. If left empty, Notification won't ben sent.
group_obj:
destination_group_obj:
allOf:
- $ref: '#/components/schemas/Group'
readOnly: true
destination_event_user:
type: boolean
description: When enabled, notification will be sent to user the user that
triggered the event.When destination_group is configured, notification
is sent to both.
required:
- group_obj
- destination_group_obj
- name
- pk
NotificationRuleRequest:
@ -49036,12 +49041,17 @@ components:
- $ref: '#/components/schemas/SeverityEnum'
description: Controls which severity level the created notifications will
have.
group:
destination_group:
type: string
format: uuid
nullable: true
description: Define which group of users this notification should be sent
and shown to. If left empty, Notification won't ben sent.
destination_event_user:
type: boolean
description: When enabled, notification will be sent to user the user that
triggered the event.When destination_group is configured, notification
is sent to both.
required:
- name
NotificationTransport:
@ -54126,12 +54136,17 @@ components:
- $ref: '#/components/schemas/SeverityEnum'
description: Controls which severity level the created notifications will
have.
group:
destination_group:
type: string
format: uuid
nullable: true
description: Define which group of users this notification should be sent
and shown to. If left empty, Notification won't ben sent.
destination_event_user:
type: boolean
description: When enabled, notification will be sent to user the user that
triggered the event.When destination_group is configured, notification
is sent to both.
PatchedNotificationTransportRequest:
type: object
description: NotificationTransport Serializer

270
web/package-lock.json generated
View File

@ -22,7 +22,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.7.2",
"@goauthentik/api": "^2025.6.2-1750242193",
"@goauthentik/api": "^2025.6.2-1750246811",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
@ -30,6 +30,8 @@
"@mdx-js/mdx": "^3.1.0",
"@mrmarble/djangoql-completion": "^0.8.3",
"@open-wc/lit-helpers": "^0.7.0",
"@openlayers-elements/core": "^0.4.0",
"@openlayers-elements/maps": "^0.4.0",
"@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.30.0",
@ -1729,9 +1731,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2025.6.2-1750242193",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750242193.tgz",
"integrity": "sha512-yAi3LbxGP1pyu/lV+iiJp2FrIhOa9/nMErdiy4KC2uh63AS74iqNpYb4/IIbkCeT8fbSSNKhAbA6gXE9zFZ6Yg=="
"version": "2025.6.2-1750246811",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.6.2-1750246811.tgz",
"integrity": "sha512-ENHEi3kGAodf5tKQb5kziUrT1EcJw3z8tp2mU7LWqNlXr4eoAI15BjDfH5DW56l4jy3xKqTd+R2Ntnj4hiVhHw=="
},
"node_modules/@goauthentik/core": {
"resolved": "packages/core",
@ -2589,6 +2591,48 @@
"@lit/reactive-element": "^1.0.0 || ^2.0.0"
}
},
"node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@mapbox/mapbox-gl-style-spec": {
"version": "13.28.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz",
"integrity": "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==",
"license": "ISC",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/unitbezier": "^0.0.0",
"csscolorparser": "~1.0.2",
"json-stringify-pretty-compact": "^2.0.0",
"minimist": "^1.2.6",
"rw": "^1.3.3",
"sort-object": "^0.3.2"
},
"bin": {
"gl-style-composite": "bin/gl-style-composite.js",
"gl-style-format": "bin/gl-style-format.js",
"gl-style-migrate": "bin/gl-style-migrate.js",
"gl-style-validate": "bin/gl-style-validate.js"
}
},
"node_modules/@mapbox/point-geometry": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
"license": "ISC"
},
"node_modules/@mapbox/unitbezier": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz",
"integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==",
"license": "BSD-2-Clause"
},
"node_modules/@mdx-js/mdx": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz",
@ -3071,6 +3115,27 @@
"lit": "^2.0.0 || ^3.0.0"
}
},
"node_modules/@openlayers-elements/core": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@openlayers-elements/core/-/core-0.4.0.tgz",
"integrity": "sha512-msY2QGYCYf5Zph16j08KszgqtHmMORCK7B5afpe5iM8c3FFSfjijUffiw93MGeowoN4Yo5jfkxuI2plpyidR0A==",
"license": "MIT",
"dependencies": {
"lit": "^3.1.4",
"ol": "^7.5.0"
}
},
"node_modules/@openlayers-elements/maps": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@openlayers-elements/maps/-/maps-0.4.0.tgz",
"integrity": "sha512-uxGW3Lt1BVA8eC0HykXLZA4a3EfCU44FdGaudC4Xu0s+XYPOEPxCGLDCsWSuy67NvEUTFb+odu6mRDLofxdquA==",
"license": "MIT",
"dependencies": {
"@openlayers-elements/core": "^0.4.0",
"lit": "^3.1.4",
"ol": "^7.5.0"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@ -3971,6 +4036,12 @@
"lit": "^3.2.1"
}
},
"node_modules/@petamoriken/float16": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz",
"integrity": "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==",
"license": "MIT"
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -12178,6 +12249,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/csscolorparser": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
"integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -13381,6 +13458,12 @@
"node": ">= 0.4"
}
},
"node_modules/earcut": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
"license": "ISC"
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -15962,6 +16045,43 @@
"node": ">=6.9.0"
}
},
"node_modules/geotiff": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz",
"integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==",
"license": "MIT",
"dependencies": {
"@petamoriken/float16": "^3.4.7",
"lerc": "^3.0.0",
"pako": "^2.0.4",
"parse-headers": "^2.0.2",
"quick-lru": "^6.1.1",
"web-worker": "^1.2.0",
"xml-utils": "^1.0.2",
"zstddec": "^0.1.0"
},
"engines": {
"node": ">=10.19"
}
},
"node_modules/geotiff/node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/geotiff/node_modules/quick-lru": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
"integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -18528,6 +18648,12 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"node_modules/json-stringify-pretty-compact": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz",
"integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==",
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -18967,6 +19093,12 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/lerc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz",
"integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==",
"license": "Apache-2.0"
},
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -19496,6 +19628,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/mapbox-to-css-font": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.5.tgz",
"integrity": "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==",
"license": "BSD-2-Clause"
},
"node_modules/markdown-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
@ -20924,7 +21062,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -22048,6 +22185,34 @@
"dev": true,
"optional": true
},
"node_modules/ol": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/ol/-/ol-7.5.2.tgz",
"integrity": "sha512-HJbb3CxXrksM6ct367LsP3N+uh+iBBMdP3DeGGipdV9YAYTP0vTJzqGnoqQ6C2IW4qf8krw9yuyQbc9fjOIaOQ==",
"license": "BSD-2-Clause",
"dependencies": {
"earcut": "^2.2.3",
"geotiff": "^2.0.7",
"ol-mapbox-style": "^10.1.0",
"pbf": "3.2.1",
"rbush": "^3.0.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/openlayers"
}
},
"node_modules/ol-mapbox-style": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-10.7.0.tgz",
"integrity": "sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw==",
"license": "BSD-2-Clause",
"dependencies": {
"@mapbox/mapbox-gl-style-spec": "^13.23.1",
"mapbox-to-css-font": "^2.4.1",
"ol": "^7.3.0"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -22378,6 +22543,12 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/parse-headers": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
"integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==",
"license": "MIT"
},
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
@ -22528,6 +22699,19 @@
"node": ">= 14.16"
}
},
"node_modules/pbf": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",
"integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==",
"license": "BSD-3-Clause",
"dependencies": {
"ieee754": "^1.1.12",
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
}
},
"node_modules/peek-readable": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz",
@ -22989,6 +23173,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/protocol-buffers-schema": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
"license": "MIT"
},
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
@ -23204,6 +23394,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
"license": "ISC"
},
"node_modules/ramda": {
"version": "0.30.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
@ -23325,6 +23521,15 @@
"node": ">= 0.8"
}
},
"node_modules/rbush": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz",
"integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==",
"license": "MIT",
"dependencies": {
"quickselect": "^2.0.0"
}
},
"node_modules/rc9": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
@ -24242,6 +24447,15 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/resolve-protobuf-schema": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
"license": "MIT",
"dependencies": {
"protocol-buffers-schema": "^3.3.1"
}
},
"node_modules/resolve.exports": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
@ -25083,6 +25297,22 @@
"node": ">= 14"
}
},
"node_modules/sort-asc": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz",
"integrity": "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sort-desc": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz",
"integrity": "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
@ -25119,6 +25349,18 @@
"node": ">=0.10.0"
}
},
"node_modules/sort-object": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz",
"integrity": "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==",
"dependencies": {
"sort-asc": "^0.1.0",
"sort-desc": "^0.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sort-object-keys": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz",
@ -28351,6 +28593,12 @@
"license": "MIT",
"optional": true
},
"node_modules/web-worker": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
"integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
"license": "Apache-2.0"
},
"node_modules/webauthn-polyfills": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/webauthn-polyfills/-/webauthn-polyfills-0.1.7.tgz",
@ -28925,6 +29173,12 @@
"repeat-string": "^1.5.2"
}
},
"node_modules/xml-utils": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
"integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==",
"license": "CC0-1.0"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@ -29182,6 +29436,12 @@
"zod": "^3.18.0"
}
},
"node_modules/zstddec": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz",
"integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==",
"license": "MIT AND BSD-3-Clause"
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@ -93,7 +93,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.7.2",
"@goauthentik/api": "^2025.6.2-1750242193",
"@goauthentik/api": "^2025.6.2-1750246811",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
@ -101,6 +101,8 @@
"@mdx-js/mdx": "^3.1.0",
"@mrmarble/djangoql-completion": "^0.8.3",
"@open-wc/lit-helpers": "^0.7.0",
"@openlayers-elements/core": "^0.4.0",
"@openlayers-elements/maps": "^0.4.0",
"@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.30.0",

View File

@ -1,3 +1,7 @@
import "#elements/Tabs";
import { WithLicenseSummary } from "#elements/mixins/license";
import { updateURLParams } from "#elements/router/RouteMatch";
import "@goauthentik/admin/events/EventMap";
import "@goauthentik/admin/events/EventVolumeChart";
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
@ -15,10 +19,12 @@ import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Event, EventsApi } from "@goauthentik/api";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import { Event, EventsApi, LicenseSummaryStatusEnum } from "@goauthentik/api";
@customElement("ak-event-list")
export class EventListPage extends TablePage<Event> {
export class EventListPage extends WithLicenseSummary(TablePage<Event>) {
expandable = true;
supportsQL = true;
@ -39,11 +45,15 @@ export class EventListPage extends TablePage<Event> {
order = "-created";
static get styles(): CSSResult[] {
return super.styles.concat(css`
// @ts-expect-error
return super.styles.concat(
PFGrid,
css`
.pf-m-no-padding-bottom {
padding-bottom: 0;
}
`);
`,
);
}
async apiEndpoint(): Promise<PaginatedResponse<Event>> {
@ -62,16 +72,39 @@ export class EventListPage extends TablePage<Event> {
}
renderSectionBefore(): TemplateResult {
return html`
<div class="pf-c-page__main-section pf-m-no-padding-bottom">
if (this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed) {
return html`<div
class="pf-l-grid pf-m-gutter pf-c-page__main-section pf-m-no-padding-bottom"
>
<ak-events-volume-chart
class="pf-l-grid__item pf-m-12-col pf-m-4-col-on-xl pf-m-4-col-on-2xl "
.query=${{
page: this.page,
search: this.search,
}}
with-map
></ak-events-volume-chart>
</div>
`;
<ak-events-map
class="pf-l-grid__item pf-m-12-col pf-m-8-col-on-xl pf-m-8-col-on-2xl "
.events=${this.data}
@select-event=${(ev: CustomEvent<{ eventId: string }>) => {
this.search = `event_uuid = "${ev.detail.eventId}"`;
this.page = 1;
updateURLParams({
search: this.search,
tablePage: this.page,
});
this.fetch();
}}
></ak-events-map>
</div>`;
}
return html`<ak-events-volume-chart
.query=${{
page: this.page,
search: this.search,
}}
></ak-events-volume-chart>`;
}
row(item: EventWithContext): SlottedTemplateResult[] {

View File

@ -0,0 +1,139 @@
import { EventWithContext } from "#common/events";
import { globalAK } from "#common/global";
import { PaginatedResponse } from "#elements/table/Table";
import { AKElement } from "@goauthentik/elements/Base";
import "@openlayers-elements/core/ol-layer-vector";
import type OlLayerVector from "@openlayers-elements/core/ol-layer-vector";
import "@openlayers-elements/core/ol-map";
import type OlMap from "@openlayers-elements/core/ol-map";
import "@openlayers-elements/maps/ol-layer-openstreetmap";
import "@openlayers-elements/maps/ol-select";
import Feature from "ol/Feature";
import { Point } from "ol/geom";
import { fromLonLat } from "ol/proj";
import Icon from "ol/style/Icon";
import Style from "ol/style/Style";
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Event } from "@goauthentik/api";
/**
*
* @event {select-event} - Fired when an event is selected on the map. ID of the event is contained
* in the `Event.detail` field.
*
*/
@customElement("ak-events-map")
export class EventMap extends AKElement {
@property({ attribute: false })
events?: PaginatedResponse<Event>;
@query("ol-layer-vector")
vectorLayer?: OlLayerVector;
@query("ol-map")
map?: OlMap;
@property({ type: Number })
zoomPaddingPx = 100;
static get styles(): CSSResult[] {
return [
PFBase,
PFCard,
css`
.pf-c-card,
ol-map {
height: 24rem;
}
:host([theme="dark"]) ol-map {
filter: invert(100%) hue-rotate(180deg);
}
`,
];
}
updated(_changedProperties: PropertyValues<this>): void {
if (!_changedProperties.has("events")) {
return;
}
if (!this.vectorLayer?.source || !this.map?.map) {
return;
}
// Remove all existing points
this.vectorLayer.source.clear();
// Re-add them
this.events?.results
.filter((event) => {
if (!Object.hasOwn(event.context, "geo")) {
return false;
}
const geo = (event as EventWithContext).context.geo;
if (!geo?.lat || !geo.long) {
return false;
}
return true;
})
.forEach((event) => {
const geo = (event as EventWithContext).context.geo!;
const point = new Point(fromLonLat([geo.long!, geo.lat!]));
const feature = new Feature({
geometry: point,
});
feature.setStyle(
new Style({
image: new Icon({
anchor: [0.5, 1],
offset: [0, 0],
opacity: 1,
scale: 1,
rotateWithView: false,
rotation: 0,
src: `${globalAK().api.base}static/dist/assets/images/map_pin.svg`,
}),
}),
);
feature.setId(event.pk);
this.vectorLayer?.source?.addFeature(feature);
});
// Zoom to show points better
this.map.map.getView().fit(this.vectorLayer.source.getExtent(), {
padding: [
this.zoomPaddingPx,
this.zoomPaddingPx,
this.zoomPaddingPx,
this.zoomPaddingPx,
],
duration: 500,
maxZoom: 4.5,
});
}
render(): TemplateResult {
return html`<div class="pf-c-card">
<ol-map>
<ol-select
@feature-selected=${(ev: CustomEvent<{ feature: Feature }>) => {
const eventId = ev.detail.feature.getId();
this.dispatchEvent(
new CustomEvent("select-event", {
composed: true,
bubbles: true,
detail: {
eventId: eventId,
},
}),
);
}}
></ol-select>
<ol-layer-openstreetmap></ol-layer-openstreetmap>
<ol-layer-vector></ol-layer-vector>
</ol-map>
</div>`;
}
}

View File

@ -11,11 +11,14 @@ import { EventVolume, EventsApi, EventsEventsListRequest } from "@goauthentik/ap
@customElement("ak-events-volume-chart")
export class EventVolumeChart extends EventChart {
@property({ attribute: "with-map", type: Boolean })
withMap = false;
_query?: EventsEventsListRequest;
@property({ attribute: false })
set query(value: EventsEventsListRequest | undefined) {
if (JSON.stringify(this._query) === JSON.stringify(value)) return;
if (JSON.stringify(value) !== JSON.stringify(this._query)) return;
this._query = value;
this.refreshHandler();
}
@ -24,6 +27,9 @@ export class EventVolumeChart extends EventChart {
return super.styles.concat(
PFCard,
css`
:host([with-map]) .pf-c-card {
height: 24rem;
}
.pf-c-card {
height: 20rem;
}

View File

@ -66,7 +66,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Group")} name="group">
<ak-form-element-horizontal label=${msg("Group")} name="destinationGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
@ -86,14 +86,44 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
return group?.pk;
}}
.selected=${(group: Group): boolean => {
return group.pk === this.instance?.group;
return group.pk === this.instance?.destinationGroup;
}}
blankable
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg("Select the group of users which the alerts are sent to. ")}
</p>
<p class="pf-c-form__helper-text">
${msg(
"Select the group of users which the alerts are sent to. If no group is selected the rule is disabled.",
"If no group is selected and 'Send notification to event user' is disabled the rule is disabled. ",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="destinationEventUser">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${this.instance?.destinationEventUser ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Send notification to event user")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"If no group is selected and 'Send notification to event user' is disabled the rule is disabled. ",
)}
</p>
</ak-form-element-horizontal>

View File

@ -3,6 +3,7 @@ import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionModal";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { severityToLabel } from "@goauthentik/common/labels";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
@ -51,6 +52,7 @@ export class RuleListPage extends TablePage<NotificationRule> {
columns(): TableColumn[] {
return [
new TableColumn(msg("Enabled")),
new TableColumn(msg("Name"), "name"),
new TableColumn(msg("Severity"), "severity"),
new TableColumn(msg("Sent to group"), "group"),
@ -81,12 +83,16 @@ export class RuleListPage extends TablePage<NotificationRule> {
}
row(item: NotificationRule): TemplateResult[] {
const enabled = !!item.destinationGroupObj || item.destinationEventUser;
return [
html`<ak-status-label type="warning" ?good=${enabled}></ak-status-label>`,
html`${item.name}`,
html`${severityToLabel(item.severity)}`,
html`${item.groupObj
? html`<a href="#/identity/groups/${item.groupObj.pk}">${item.groupObj.name}</a>`
: msg("None (rule disabled)")}`,
html`${item.destinationGroupObj
? html`<a href="#/identity/groups/${item.destinationGroupObj.pk}"
>${item.destinationGroupObj.name}</a
>`
: msg("-")}`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Notification Rule")} </span>

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="5.6444445mm"
height="9.847393mm"
viewBox="0 0 20 34.892337"
id="svg3455"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="Map Pin.svg">
<defs
id="defs3457" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="12.181359"
inkscape:cx="8.4346812"
inkscape:cy="14.715224"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1024"
inkscape:window-height="705"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata3460">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-814.59595,-274.38623)">
<g
id="g3477"
transform="matrix(1.1855854,0,0,1.1855854,-151.17715,-57.3976)">
<path
sodipodi:nodetypes="sscccccsscs"
inkscape:connector-curvature="0"
id="path4337-3"
d="m 817.11249,282.97118 c -1.25816,1.34277 -2.04623,3.29881 -2.01563,5.13867 0.0639,3.84476 1.79693,5.3002 4.56836,10.59179 0.99832,2.32851 2.04027,4.79237 3.03125,8.87305 0.13772,0.60193 0.27203,1.16104 0.33416,1.20948 0.0621,0.0485 0.19644,-0.51262 0.33416,-1.11455 0.99098,-4.08068 2.03293,-6.54258 3.03125,-8.87109 2.77143,-5.29159 4.50444,-6.74704 4.56836,-10.5918 0.0306,-1.83986 -0.75942,-3.79785 -2.01758,-5.14062 -1.43724,-1.53389 -3.60504,-2.66908 -5.91619,-2.71655 -2.31115,-0.0475 -4.4809,1.08773 -5.91814,2.62162 z"
style="display:inline;opacity:1;fill:#fd4b2d;fill-opacity:1;" />
<circle
r="3.0355"
cy="288.25278"
cx="823.03064"
id="path3049"
style="display:inline;opacity:1;fill:#590000;fill-opacity:1;stroke-width:0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -12,6 +12,8 @@ export interface EventGeo {
city?: string;
country?: string;
continent?: string;
lat?: number;
long?: number;
}
export interface EventModel {

View File

@ -76,7 +76,6 @@ export class ObjectChangelog extends Table<Event> {
html`<div>${formatElapsedTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`,
html`<div>${item.clientIp || msg("-")}</div>
<small>${EventGeo(item)}</small>`,
];
}