mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
enterprise/reports: add users and events export (#18088)
* enterprise: add users and events export (reports app) * enterprise/reports: replace assert with AsertionError so that the assumption check is not lost when compiling to optimised byte code * enterprise/reports: use ConditionalInheritance with ExportMixin to make reduce coupling of enterprise with the rest of authentik * enterprise/reports: use custom iterative File to save data export instead of accessing default_storage directly, so all the FileField.save logic can run correctly (e.g. creating directories) * enterprise/reports: change app label to simply "authentik_reports" * wip Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * update for new file api Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * Apply suggestions from code review Signed-off-by: Dominic R <dominic@sdko.org> * wip * sources/oauth: save returned oauth refresh tokens and add slack provider (#18501) * sources/oauth: save returned oauth refresh tokens * Update authentik/sources/oauth/models.py Co-authored-by: Jens L. <jens@goauthentik.io> Signed-off-by: Connor Peshek <connor@connorpeshek.me> * lint * add tests * fix proper id setting * update id test --------- Signed-off-by: Connor Peshek <connor@connorpeshek.me> Co-authored-by: connor peshek <connorpeshek@unknown1641287c8f5d.attlocal.net> Co-authored-by: Jens L. <jens@goauthentik.io> Co-authored-by: connor peshek <connorpeshek@connors-MacBook-Pro.local> * core: custom avatar url improvements (#10525) Co-authored-by: Dominic R <dominic@sdko.org> * website/integrations: add salesforce (#18516) Co-authored-by: connor peshek <connorpeshek@connors-MacBook-Pro.local> Co-authored-by: dewi-tik <dewi@goauthentik.io> Co-authored-by: Dominic R <dominic@sdko.org> * endpoints: implement endpoint stage (#18468) * endpoints: implement endpoint stage Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix mismatched label Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix url in mdm config Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rephrase Signed-off-by: Jens Langhammer <jens@goauthentik.io> * and API & UI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add deprecated support and deprecate gdtc Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add stage mode Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework stage slightly, add frontend Signed-off-by: Jens Langhammer <jens@goauthentik.io> * include jwks, add iat and exp Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set kid Signed-off-by: Jens Langhammer <jens@goauthentik.io> * include device details in event list Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * implement device summary Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add remaining tables Signed-off-by: Jens Langhammer <jens@goauthentik.io> * revert sanitize Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix uuid format issues Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web/flows: update default background image (#18540) Signed-off-by: Jens Langhammer <jens@goauthentik.io> * website/integrations: add hoop.dev (#17868) Co-authored-by: iops <iops@syneforge.com> Co-authored-by: Dominic R <dominic@sdko.org> * website: Docusaurus 3.9.2 (#18506) * endpoints/stage: v2, better error handling, more settings (#18545) * add options, idle fallback Signed-off-by: Jens Langhammer <jens@goauthentik.io> * delete other device tokens during enroll Signed-off-by: Jens Langhammer <jens@goauthentik.io> * better error handling Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> * website: Glossary (#16007) * website: Glossary fix minor issues wip Apply suggestion from @dominic-r Signed-off-by: Dominic R <dominic@sdko.org> anchor to param wip wip at least the lockfile changes now sure a-z first as tana asked idk why i switched in the first place wip wip lock lockfiles are hard wip please work no have? Revert "no have?" This reverts commit 743dbc1bc2900eedcc2c93af248e6afdec3688a3. * changed to sentence-case capitalization --------- Co-authored-by: Tana M Berry <tana@goauthentik.io> * web/i18n: Locale Context Merge Branch (#18426) * web: Update fonts to Patternfly 5 variants. * Fix order of heading override. * web: Flesh out locale context. * Fix Han pattern. * Remove comment. * Add additional regional codes. * Clarify comment. * Fix typos. * web/i18n: Add locale-specific font overrides. * Fix stale session in locale lifecycle. * core, web: Fix Han language codes. * Fix warnings about invalid BCP language code. * Build translations. * Add locale relative labels. * Add locale translations for Finnish and Portuguese. * Fix XLIFF errors. * Clean up labels. * Tidy regions. * Match region comment. * Update extracted values. * Fix locale switch not triggering on source language. * Split labels. * Clean up labels. * providers/scim: cache ServiceProviderConfig (#18047) * Update authentik/enterprise/reports/api/reports.py Co-authored-by: Jens L. <jens@beryju.org> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * enterprise/reports: got rid of unnecessary method-level import * enterprise/reports: celan up code duplication in data export generation (invoke viewset.filter_queryset directly instead of replicating it) * enterprise/reports: add check for app label when switching on content types * enterprise/reports: make hyperlink field on Notification larger so it can fit the security token in the export file URL * enterprise/reports: add is_superuser back in users export * enterprise/reports: split tests into multiple files * Apply suggestions from code review Signed-off-by: Dewi Roberts <dewi@goauthentik.io> * Fixed prettier issue * Update web/src/admin/events/DataExportListPage.ts Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update web/src/admin/events/DataExportListPage.ts Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update web/src/admin/events/EventListPage.ts Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update web/src/admin/reports/ExportButton.ts Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update web/src/admin/reports/ExportButton.ts Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update web/src/admin/users/UserListPage.ts Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update web/src/elements/notifications/NotificationDrawer.ts Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update web/src/elements/sidebar/SidebarItem.css Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * enterprise/reports: resolve code review merge errors * enterprise/reports: remove the export button from the dom flow (by settings display:none) when there's no license * enterprise/reports: improve docs * include notification link in email Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * enterprise/reports: remove assignment assertion in ExportButton.ts * cleanup tests after perm update Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Signed-off-by: Dominic R <dominic@sdko.org> Signed-off-by: Connor Peshek <connor@connorpeshek.me> Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> Signed-off-by: Dewi Roberts <dewi@goauthentik.io> Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Co-authored-by: Dominic R <dominic@sdko.org> Co-authored-by: Connor Peshek <connor@connorpeshek.me> Co-authored-by: connor peshek <connorpeshek@unknown1641287c8f5d.attlocal.net> Co-authored-by: Jens L. <jens@goauthentik.io> Co-authored-by: connor peshek <connorpeshek@connors-MacBook-Pro.local> Co-authored-by: Konrad Mösch <konrad@moesch.org> Co-authored-by: dewi-tik <dewi@goauthentik.io> Co-authored-by: shcherbak <ju.shcherbak@gmail.com> Co-authored-by: iops <iops@syneforge.com> Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Co-authored-by: Tana M Berry <tana@goauthentik.io> Co-authored-by: Jens L. <jens@beryju.org>
This commit is contained in:
committed by
GitHub
parent
ea513f2ec0
commit
7e9e0a87f7
@@ -86,6 +86,7 @@ from authentik.flows.models import FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||
from authentik.lib.avatars import get_avatar
|
||||
from authentik.lib.utils.reflection import ConditionalInheritance
|
||||
from authentik.rbac.api.roles import RoleSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.models import Role, get_permission_choices
|
||||
@@ -484,7 +485,11 @@ class UsersFilter(FilterSet):
|
||||
]
|
||||
|
||||
|
||||
class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
class UserViewSet(
|
||||
ConditionalInheritance("authentik.enterprise.reports.api.reports.ExportMixin"),
|
||||
UsedByMixin,
|
||||
ModelViewSet,
|
||||
):
|
||||
"""User Viewset"""
|
||||
|
||||
queryset = User.objects.none()
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.permissions import BasePermission
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||
from authentik.enterprise.reports.models import DataExport
|
||||
from authentik.enterprise.reports.tasks import generate_export
|
||||
from authentik.rbac.permissions import HasPermission
|
||||
|
||||
|
||||
class RequestedBySerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("pk", "username")
|
||||
|
||||
|
||||
class ContentTypeSerializer(ModelSerializer):
|
||||
app_label = CharField(read_only=True)
|
||||
model = CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ("id", "app_label", "model")
|
||||
|
||||
|
||||
class DataExportSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||
requested_by = RequestedBySerializer(read_only=True)
|
||||
content_type = ContentTypeSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DataExport
|
||||
fields = (
|
||||
"id",
|
||||
"requested_by",
|
||||
"requested_on",
|
||||
"content_type",
|
||||
"query_params",
|
||||
"file_url",
|
||||
"completed",
|
||||
)
|
||||
read_only_fields = (
|
||||
"id",
|
||||
"requested_by",
|
||||
"requested_on",
|
||||
"content_type",
|
||||
"file_url",
|
||||
"completed",
|
||||
)
|
||||
|
||||
|
||||
class DataExportViewSet(
|
||||
mixins.RetrieveModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet
|
||||
):
|
||||
queryset = DataExport.objects.all()
|
||||
serializer_class = DataExportSerializer
|
||||
owner_field = "requested_by"
|
||||
ordering_fields = ["completed", "requested_by", "requested_on", "content_type__model"]
|
||||
ordering = ["-requested_on"]
|
||||
search_fields = ["requested_by__username", "content_type__model"]
|
||||
|
||||
def get_queryset(self) -> QuerySet[DataExport]:
|
||||
"""Limit to exports of content types the user has view permission on"""
|
||||
qs = super().get_queryset()
|
||||
permitted_cts = []
|
||||
for ct in ContentType.objects.filter(
|
||||
id__in=qs.values_list("content_type_id", flat=True).distinct()
|
||||
):
|
||||
model = ct.model_class()
|
||||
if model is None:
|
||||
continue
|
||||
perm = f"{ct.app_label}.view_{ct.model}"
|
||||
if self.request.user.has_perm(perm):
|
||||
permitted_cts.append(ct)
|
||||
return qs.filter(content_type__in=permitted_cts)
|
||||
|
||||
|
||||
class ExportMixin:
|
||||
@extend_schema(
|
||||
request=None,
|
||||
parameters=[],
|
||||
responses={201: DataExportSerializer},
|
||||
filters=True,
|
||||
)
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["POST"],
|
||||
permission_classes=[HasPermission("authentik_reports.add_dataexport")],
|
||||
)
|
||||
def export(self: GenericViewSet, request: Request) -> Response:
|
||||
"""
|
||||
Create a data export for this data type. Note that the export is generated asynchronously:
|
||||
this method returns a `DataExport` object that will initially have `completed=false` as well
|
||||
as the permanent URL to that object in the `Location` header.
|
||||
You can poll that URL until `completed=true`, at which point the `file_url` property will
|
||||
contain a URL to download
|
||||
"""
|
||||
|
||||
s = DataExportSerializer(data={"query_params": request.query_params.dict()})
|
||||
s.is_valid(raise_exception=True)
|
||||
export = s.save(
|
||||
requested_by=request.user,
|
||||
content_type=ContentType.objects.get_for_model(self.queryset.model),
|
||||
)
|
||||
generate_export.send(export.id)
|
||||
|
||||
set = export.serializer(instance=export)
|
||||
|
||||
return Response(
|
||||
set.data,
|
||||
status=201,
|
||||
headers={"Location": reverse("authentik_api:dataexport-detail", args=[export.id])},
|
||||
)
|
||||
|
||||
def get_permissions(self: GenericViewSet) -> list[BasePermission]:
|
||||
perms = super().get_permissions()
|
||||
if self.action == "export":
|
||||
model = self.get_queryset().model
|
||||
perms.append(HasPermission(f"{model._meta.app_label}.view_{model._meta.model_name}")())
|
||||
return perms
|
||||
@@ -0,0 +1,8 @@
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class ReportsConfig(EnterpriseConfig):
|
||||
name = "authentik.enterprise.reports"
|
||||
label = "authentik_reports"
|
||||
verbose_name = "authentik Enterprise.Reports"
|
||||
default = True
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-02 17:19
|
||||
|
||||
import authentik.admin.files.fields
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DataExport",
|
||||
fields=[
|
||||
("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
("requested_on", models.DateTimeField(auto_now_add=True)),
|
||||
("query_params", models.JSONField()),
|
||||
("file", authentik.admin.files.fields.FileField(blank=True)),
|
||||
("completed", models.BooleanField(default=False)),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
|
||||
),
|
||||
),
|
||||
(
|
||||
"requested_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Data Export",
|
||||
"verbose_name_plural": "Data Exports",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
import csv
|
||||
import io
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.admin.files.fields import FileField
|
||||
from authentik.admin.files.manager import get_file_manager
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.core.models import User
|
||||
from authentik.enterprise.reports.utils import MockRequest
|
||||
from authentik.events.models import Event, EventAction, Notification, NotificationSeverity
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.db import chunked_queryset
|
||||
from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
|
||||
class DataExport(SerializerModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4)
|
||||
requested_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
|
||||
requested_on = models.DateTimeField(auto_now_add=True)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
query_params = models.JSONField()
|
||||
file = FileField(blank=True)
|
||||
completed = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Data Export")
|
||||
verbose_name_plural = _("Data Exports")
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
"""Get serializer for this model"""
|
||||
from authentik.enterprise.reports.api.reports import DataExportSerializer
|
||||
|
||||
return DataExportSerializer
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.completed:
|
||||
raise AssertionError("Data export must only be generated once")
|
||||
|
||||
model_class = self.content_type.model_class()
|
||||
model_verbose_name = model_class._meta.verbose_name
|
||||
model_verbose_name_plural = model_class._meta.verbose_name_plural
|
||||
|
||||
queryset = chunked_queryset(self.get_queryset())
|
||||
|
||||
serializer = self.get_serializer_class()(
|
||||
context={"request": self._get_request()}, instance=queryset, many=True
|
||||
)
|
||||
self.file = f"{model_verbose_name_plural.lower()}_{self.id}.csv"
|
||||
|
||||
with get_file_manager(FileUsage.REPORTS).save_file_stream(self.file) as f:
|
||||
with io.TextIOWrapper(f, encoding="utf-8", newline="") as text:
|
||||
writer = csv.writer(text)
|
||||
fields = [field.label for field in serializer.child.fields.values()]
|
||||
writer.writerow(fields)
|
||||
for record in queryset:
|
||||
data = serializer.child.to_representation(record).values()
|
||||
writer.writerow(data)
|
||||
self.completed = True
|
||||
self.save()
|
||||
|
||||
message = _(f"{model_verbose_name} export generated successfully")
|
||||
e = Event.new(
|
||||
EventAction.EXPORT_READY,
|
||||
message=message,
|
||||
export=self,
|
||||
).set_user(self.requested_by)
|
||||
e.save()
|
||||
Notification.objects.create(
|
||||
event=e,
|
||||
severity=NotificationSeverity.NOTICE,
|
||||
body=message,
|
||||
hyperlink=self.file_url,
|
||||
hyperlink_label=_("Download"),
|
||||
user=self.requested_by,
|
||||
)
|
||||
|
||||
@property
|
||||
def file_url(self) -> str:
|
||||
return get_file_manager(FileUsage.REPORTS).file_url(self.file)
|
||||
|
||||
def _get_request(self) -> MockRequest:
|
||||
return MockRequest(
|
||||
user=self.requested_by, query_params=self.query_params, tenant=get_current_tenant()
|
||||
)
|
||||
|
||||
def get_queryset(self) -> models.QuerySet:
|
||||
request = self._get_request()
|
||||
viewset = self.get_viewset()
|
||||
viewset.request = request
|
||||
queryset = viewset.get_queryset()
|
||||
queryset = viewset.filter_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_viewset(self) -> ModelViewSet:
|
||||
from authentik.core.api.users import UserViewSet
|
||||
from authentik.events.api.events import EventViewSet
|
||||
|
||||
model = (self.content_type.app_label, self.content_type.model)
|
||||
if model == ("authentik_core", "user"):
|
||||
return UserViewSet()
|
||||
elif model == ("authentik_events", "event"):
|
||||
return EventViewSet()
|
||||
raise NotImplementedError(f"Unsupported data export type {self.content_type.model}")
|
||||
|
||||
def get_serializer_class(self) -> type[Serializer]:
|
||||
from authentik.enterprise.reports.serializers import (
|
||||
ExportEventSerializer,
|
||||
ExportUserSerializer,
|
||||
)
|
||||
|
||||
if self.content_type.model == "user":
|
||||
return ExportUserSerializer
|
||||
elif self.content_type.model == "event":
|
||||
return ExportEventSerializer
|
||||
return self.get_viewset().get_serializer_class()
|
||||
@@ -0,0 +1,32 @@
|
||||
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
|
||||
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.events.api.events import EventSerializer
|
||||
|
||||
|
||||
class ExportUserSerializer(UserSerializer):
|
||||
"""Serializer for exporting users"""
|
||||
|
||||
groups = SerializerMethodField(source="get_groups")
|
||||
|
||||
def get_groups(self, instance: User) -> str:
|
||||
return ",".join([group.name for group in instance.ak_groups.all()])
|
||||
|
||||
class Meta(UserSerializer.Meta):
|
||||
fields = [f for f in UserSerializer.Meta.fields if f != "groups_obj"] + ["groups"]
|
||||
|
||||
|
||||
class ExportEventSerializer(EventSerializer):
|
||||
"""Serializer for exporting events"""
|
||||
|
||||
user_pk = IntegerField(source="user.pk", read_only=True)
|
||||
username = CharField(source="user.username", read_only=True)
|
||||
email = CharField(source="user.email", read_only=True)
|
||||
|
||||
class Meta(EventSerializer.Meta):
|
||||
fields = [f for f in EventSerializer.Meta.fields if f != "user"] + [
|
||||
"user_pk",
|
||||
"username",
|
||||
"email",
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dramatiq import actor
|
||||
|
||||
from authentik.enterprise.reports.models import DataExport
|
||||
|
||||
|
||||
@actor(description=_("Generate data export."))
|
||||
def generate_export(export_id: int):
|
||||
export = DataExport.objects.get(id=export_id)
|
||||
export.generate()
|
||||
@@ -0,0 +1,53 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.enterprise.reports.tests.utils import patch_license
|
||||
from authentik.events.models import Event
|
||||
|
||||
|
||||
@patch_license
|
||||
class TestExportAPI(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_create_user_export(self):
|
||||
"""Test User export endpoint"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-export"),
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(
|
||||
response.headers["Location"],
|
||||
reverse("authentik_api:dataexport-detail", kwargs={"pk": response.data["id"]}),
|
||||
)
|
||||
self.assertEqual(response.data["requested_by"]["pk"], self.user.pk)
|
||||
self.assertEqual(response.data["completed"], False)
|
||||
self.assertEqual(response.data["file_url"], "")
|
||||
self.assertEqual(response.data["query_params"], {})
|
||||
self.assertEqual(
|
||||
response.data["content_type"]["id"],
|
||||
ContentType.objects.get_for_model(User).id,
|
||||
)
|
||||
|
||||
def test_create_event_export(self):
|
||||
"""Test Event export endpoint"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:event-export"),
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(
|
||||
response.headers["Location"],
|
||||
reverse("authentik_api:dataexport-detail", kwargs={"pk": response.data["id"]}),
|
||||
)
|
||||
self.assertEqual(response.data["requested_by"]["pk"], self.user.pk)
|
||||
self.assertEqual(response.data["completed"], False)
|
||||
self.assertEqual(response.data["file_url"], "")
|
||||
self.assertEqual(response.data["query_params"], {})
|
||||
self.assertEqual(
|
||||
response.data["content_type"]["id"],
|
||||
ContentType.objects.get_for_model(Event).id,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.enterprise.reports.models import DataExport
|
||||
from authentik.enterprise.reports.tests.utils import patch_license
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
||||
@patch_license
|
||||
class TestEventExport(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_user()
|
||||
self.user.assign_perms_to_managed_role("authentik_events.view_event")
|
||||
|
||||
self.e1 = Event.new(EventAction.LOGIN, user=self.user)
|
||||
self.e1.save()
|
||||
self.e2 = Event.new(EventAction.LOGIN_FAILED, user=self.user)
|
||||
self.e2.save()
|
||||
|
||||
def test_type_filter(self):
|
||||
export = DataExport.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(Event),
|
||||
requested_by=self.user,
|
||||
query_params={"actions": [EventAction.LOGIN]},
|
||||
)
|
||||
records = list(export.get_queryset())
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(records[0], self.e1)
|
||||
@@ -0,0 +1,80 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.enterprise.reports.tests.utils import patch_license
|
||||
|
||||
|
||||
@patch_license
|
||||
class TestExportPermissions(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = create_test_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_export_without_permission(self):
|
||||
"""Test User export endpoint without permission"""
|
||||
response = self.client.post(reverse("authentik_api:user-export"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_export_only_user_permission(self):
|
||||
"""Test User export endpoint with only view_user permission"""
|
||||
self.user.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
response = self.client.post(reverse("authentik_api:user-export"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_export_with_permission(self):
|
||||
"""Test User export endpoint with view_user and add_dataexport permission"""
|
||||
self.user.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.user.assign_perms_to_managed_role("authentik_reports.add_dataexport")
|
||||
response = self.client.post(reverse("authentik_api:user-export"))
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
def test_export_access(self):
|
||||
"""Test that data export access is restricted to the user who created it"""
|
||||
self.user.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.user.assign_perms_to_managed_role("authentik_reports.add_dataexport")
|
||||
response = self.client.post(reverse("authentik_api:user-export"))
|
||||
self.assertEqual(response.status_code, 201)
|
||||
export_url = reverse("authentik_api:dataexport-detail", kwargs={"pk": response.data["id"]})
|
||||
response = self.client.get(export_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
other_user = create_test_user()
|
||||
other_user.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
other_user.assign_perms_to_managed_role("authentik_reports.add_dataexport")
|
||||
self.client.logout()
|
||||
self.client.force_login(other_user)
|
||||
response = self.client.get(export_url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_export_access_no_datatype_permission(self):
|
||||
"""Test that data export access requires view permission on the data type"""
|
||||
self.user.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.user.assign_perms_to_managed_role("authentik_reports.add_dataexport")
|
||||
self.user.assign_perms_to_managed_role("authentik_reports.view_dataexport")
|
||||
response = self.client.post(reverse("authentik_api:user-export"))
|
||||
self.assertEqual(response.status_code, 201)
|
||||
export_url = reverse("authentik_api:dataexport-detail", kwargs={"pk": response.data["id"]})
|
||||
|
||||
response = self.client.get(export_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.user.remove_perms_from_managed_role("authentik_core.view_user")
|
||||
response = self.client.get(export_url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
response = self.client.get(reverse("authentik_api:dataexport-list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.data["results"]), 0)
|
||||
|
||||
def test_export_access_owner(self):
|
||||
self.user.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.user.assign_perms_to_managed_role("authentik_reports.add_dataexport")
|
||||
response = self.client.post(reverse("authentik_api:user-export"))
|
||||
self.assertEqual(response.status_code, 201)
|
||||
export_url = reverse("authentik_api:dataexport-detail", kwargs={"pk": response.data["id"]})
|
||||
response = self.client.get(export_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.user.remove_perms_from_managed_role("authentik_core.view_user")
|
||||
response = self.client.get(export_url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
@@ -0,0 +1,48 @@
|
||||
from django.test.testcases import TestCase
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
|
||||
from authentik.enterprise.reports.tests.utils import patch_license
|
||||
|
||||
|
||||
@patch_license
|
||||
class TestSchemaMatch(TestCase):
|
||||
def setUp(self) -> None:
|
||||
generator = SchemaGenerator()
|
||||
self.schema = generator.get_schema(request=None, public=True)
|
||||
|
||||
def _index_params_by_name(self, parameters):
|
||||
result = {}
|
||||
for p in parameters or []:
|
||||
if p.get("in") != "query":
|
||||
continue
|
||||
schema = p.get("schema", {})
|
||||
result[p["name"]] = {
|
||||
"required": p.get("required", False),
|
||||
"type": schema.get("type"),
|
||||
"format": schema.get("format"),
|
||||
"enum": tuple(schema.get("enum", [])),
|
||||
}
|
||||
return result
|
||||
|
||||
def _find_operation_by_operation_id(self, operation_id):
|
||||
for path_item in self.schema.get("paths", {}).values():
|
||||
for operation in path_item.values():
|
||||
if isinstance(operation, dict) and operation.get("operationId") == operation_id:
|
||||
return operation
|
||||
raise AssertionError(f"operationId '{operation_id}' not found in schema")
|
||||
|
||||
def _get_op_params(self, operation_id):
|
||||
operation = self._find_operation_by_operation_id(operation_id)
|
||||
return self._index_params_by_name(operation.get("parameters", []))
|
||||
|
||||
def test_user_export_action_query_params_match_list(self):
|
||||
list_params = self._get_op_params("core_users_list")
|
||||
del list_params["include_groups"] # Not applicable for export
|
||||
del list_params["include_roles"] # Not applicable for export
|
||||
export_params = self._get_op_params("core_users_export_create")
|
||||
self.assertDictEqual(list_params, export_params)
|
||||
|
||||
def test_event_export_action_query_params_match_list(self):
|
||||
list_params = self._get_op_params("events_events_list")
|
||||
export_params = self._get_op_params("events_events_export_create")
|
||||
self.assertDictEqual(list_params, export_params)
|
||||
@@ -0,0 +1,75 @@
|
||||
import csv
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from authentik.admin.files.tests.utils import FileTestFileBackendMixin
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.enterprise.reports.models import DataExport
|
||||
from authentik.enterprise.reports.tests.utils import patch_license
|
||||
|
||||
|
||||
@patch_license
|
||||
class TestUserExport(FileTestFileBackendMixin, TestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
self.u1 = create_test_user(username="a")
|
||||
self.u1.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
self.u2 = create_test_user(username="b", path="abcd")
|
||||
self.u1.assign_perms_to_managed_role("authentik_core.view_user")
|
||||
|
||||
def _read_export(self, filename):
|
||||
with open(f"{self.reports_backend_path}/reports/public/{filename}") as f:
|
||||
reader = csv.DictReader(f)
|
||||
return list(reader)
|
||||
|
||||
def test_generate_user_export(self):
|
||||
export = DataExport.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(User),
|
||||
requested_by=self.u1,
|
||||
query_params={"email": str(self.u1.email)},
|
||||
)
|
||||
export.generate()
|
||||
|
||||
self.assertEqual(export.completed, True)
|
||||
data = self._read_export(export.file)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(data[0]["Username"], self.u1.username)
|
||||
|
||||
def test_path_filter(self):
|
||||
export = DataExport.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(User),
|
||||
requested_by=self.u1,
|
||||
query_params={"path": str(self.u2.path)},
|
||||
)
|
||||
records = list(export.get_queryset())
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(records[0], self.u2)
|
||||
|
||||
def test_search_filter(self):
|
||||
export = DataExport.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(User),
|
||||
requested_by=self.u1,
|
||||
query_params={"search": f'username = "{self.u2.username}"'},
|
||||
)
|
||||
records = list(export.get_queryset())
|
||||
self.assertEqual(len(records), 1)
|
||||
self.assertEqual(records[0], self.u2)
|
||||
|
||||
def test_ordering(self):
|
||||
export = DataExport.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(User),
|
||||
requested_by=self.u1,
|
||||
query_params={"ordering": "-username"},
|
||||
)
|
||||
records = list(export.get_queryset())
|
||||
self.assertGreaterEqual(records[0].username, records[-1].username)
|
||||
export = DataExport.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(User),
|
||||
requested_by=self.u1,
|
||||
query_params={"ordering": "username"},
|
||||
)
|
||||
records = list(export.get_queryset())
|
||||
self.assertLess(records[0].username, records[-1].username)
|
||||
@@ -0,0 +1,6 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
patch_license = patch(
|
||||
"authentik.enterprise.models.LicenseUsageStatus.is_valid",
|
||||
MagicMock(return_value=True),
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""API URLs"""
|
||||
|
||||
from authentik.enterprise.reports.api.reports import DataExportViewSet
|
||||
|
||||
api_urlpatterns = [
|
||||
("reports/exports", DataExportViewSet),
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockRequest:
|
||||
user: User
|
||||
query_params: dict[str, str]
|
||||
tenant: Tenant
|
||||
@@ -9,6 +9,7 @@ TENANT_APPS = [
|
||||
"authentik.enterprise.providers.radius",
|
||||
"authentik.enterprise.providers.scim",
|
||||
"authentik.enterprise.providers.ssf",
|
||||
"authentik.enterprise.reports",
|
||||
"authentik.enterprise.search",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
"authentik.enterprise.stages.mtls",
|
||||
|
||||
@@ -21,6 +21,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.core.api.object_types import TypeCreateSerializer
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.reflection import ConditionalInheritance
|
||||
|
||||
|
||||
class EventVolumeSerializer(PassiveSerializer):
|
||||
@@ -116,7 +117,9 @@ class EventsFilter(django_filters.FilterSet):
|
||||
fields = ["action", "client_ip", "username"]
|
||||
|
||||
|
||||
class EventViewSet(ModelViewSet):
|
||||
class EventViewSet(
|
||||
ConditionalInheritance("authentik.enterprise.reports.api.reports.ExportMixin"), ModelViewSet
|
||||
):
|
||||
"""Event Read-Only Viewset"""
|
||||
|
||||
queryset = Event.objects.all()
|
||||
|
||||
@@ -29,6 +29,8 @@ class NotificationSerializer(ModelSerializer):
|
||||
"pk",
|
||||
"severity",
|
||||
"body",
|
||||
"hyperlink",
|
||||
"hyperlink_label",
|
||||
"created",
|
||||
"event",
|
||||
"seen",
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-05 10:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0013_delete_systemtask"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="notification",
|
||||
name="hyperlink",
|
||||
field=models.TextField(blank=True, max_length=4096, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notification",
|
||||
name="hyperlink_label",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
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"),
|
||||
("secret_view", "Secret View"),
|
||||
("secret_rotate", "Secret Rotate"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("flow_execution", "Flow Execution"),
|
||||
("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"),
|
||||
("system_exception", "System Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("email_sent", "Email Sent"),
|
||||
("update_available", "Update Available"),
|
||||
("export_ready", "Export Ready"),
|
||||
("custom_", "Custom Prefix"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -117,6 +117,8 @@ class EventAction(models.TextChoices):
|
||||
|
||||
UPDATE_AVAILABLE = "update_available"
|
||||
|
||||
EXPORT_READY = "export_ready"
|
||||
|
||||
CUSTOM_PREFIX = "custom_"
|
||||
|
||||
|
||||
@@ -261,6 +263,14 @@ class Event(SerializerModel, ExpiringModel):
|
||||
return self.context["message"]
|
||||
return f"{self.action}: {self.context}"
|
||||
|
||||
@property
|
||||
def hyperlink(self) -> str | None:
|
||||
return self.context.get("hyperlink")
|
||||
|
||||
@property
|
||||
def hyperlink_label(self) -> str | None:
|
||||
return self.context.get("hyperlink_label")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Event action={self.action} user={self.user} context={self.context}"
|
||||
|
||||
@@ -479,6 +489,11 @@ class NotificationTransport(TasksModel, SerializerModel):
|
||||
context["key_value"]["event_user_username"] = notification.event.user.get(
|
||||
"username", None
|
||||
)
|
||||
if notification.hyperlink:
|
||||
context["link"] = {
|
||||
"target": notification.hyperlink,
|
||||
"label": notification.hyperlink_label,
|
||||
}
|
||||
if notification.event:
|
||||
context["title"] += notification.event.action
|
||||
for key, value in notification.event.context.items():
|
||||
@@ -532,6 +547,8 @@ class Notification(SerializerModel):
|
||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
severity = models.TextField(choices=NotificationSeverity.choices)
|
||||
body = models.TextField()
|
||||
hyperlink = models.TextField(blank=True, null=True, max_length=4096)
|
||||
hyperlink_label = models.TextField(blank=True, null=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
event = models.ForeignKey(Event, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
seen = models.BooleanField(default=False)
|
||||
|
||||
@@ -110,7 +110,12 @@ def notification_transport(transport_pk: int, event_pk: str, user_pk: int, trigg
|
||||
if not trigger:
|
||||
return
|
||||
notification = Notification(
|
||||
severity=trigger.severity, body=event.summary, event=event, user=user
|
||||
severity=trigger.severity,
|
||||
body=event.summary,
|
||||
event=event,
|
||||
user=user,
|
||||
hyperlink=event.hyperlink,
|
||||
hyperlink_label=event.hyperlink_label,
|
||||
)
|
||||
transport: NotificationTransport = NotificationTransport.objects.filter(pk=transport_pk).first()
|
||||
if not transport:
|
||||
|
||||
@@ -26,4 +26,4 @@ def chunked_queryset(queryset: QuerySet, chunk_size: int = 1_000):
|
||||
for chunk in get_chunks(queryset):
|
||||
reset_queries()
|
||||
gc.collect()
|
||||
yield from chunk.iterator()
|
||||
yield from chunk.iterator(chunk_size=chunk_size)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 5.1.12 on 2025-10-02 12:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0023_alter_eventmatcherpolicy_action_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
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"),
|
||||
("secret_view", "Secret View"),
|
||||
("secret_rotate", "Secret Rotate"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("flow_execution", "Flow Execution"),
|
||||
("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"),
|
||||
("system_exception", "System Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("email_sent", "Email Sent"),
|
||||
("update_available", "Update Available"),
|
||||
("export_ready", "Export Ready"),
|
||||
("custom_", "Custom Prefix"),
|
||||
],
|
||||
default=None,
|
||||
help_text="Match created events with this action type. When left empty, all action types will be matched.",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -18,6 +18,13 @@
|
||||
{{ body }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if link %}
|
||||
<tr>
|
||||
<td align="center" class="btn btn-primary">
|
||||
<a id="confirm" href="{{ link.target }}" rel="noopener noreferrer" target="_blank">{{ link.label }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if key_value %}
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@@ -896,6 +896,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_reports.dataexport"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"created",
|
||||
"must_created",
|
||||
"present"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_reports.dataexport_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_reports.dataexport"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_reports.dataexport"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -5617,6 +5657,10 @@
|
||||
"authentik_rbac.view_role",
|
||||
"authentik_rbac.view_system_info",
|
||||
"authentik_rbac.view_system_settings",
|
||||
"authentik_reports.add_dataexport",
|
||||
"authentik_reports.change_dataexport",
|
||||
"authentik_reports.delete_dataexport",
|
||||
"authentik_reports.view_dataexport",
|
||||
"authentik_sources_kerberos.add_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.add_kerberossource",
|
||||
"authentik_sources_kerberos.add_kerberossourcepropertymapping",
|
||||
@@ -6914,6 +6958,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_reports.dataexport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query_params": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Query params"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_reports.dataexport_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_dataexport",
|
||||
"change_dataexport",
|
||||
"delete_dataexport",
|
||||
"view_dataexport"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7363,6 +7444,7 @@
|
||||
"model_deleted",
|
||||
"email_sent",
|
||||
"update_available",
|
||||
"export_ready",
|
||||
"custom_"
|
||||
],
|
||||
"title": "Action"
|
||||
@@ -7427,6 +7509,21 @@
|
||||
"model_authentik_events.notification": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hyperlink": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"maxLength": 4096,
|
||||
"title": "Hyperlink"
|
||||
},
|
||||
"hyperlink_label": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"title": "Hyperlink label"
|
||||
},
|
||||
"event": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7464,6 +7561,7 @@
|
||||
"model_deleted",
|
||||
"email_sent",
|
||||
"update_available",
|
||||
"export_ready",
|
||||
"custom_"
|
||||
],
|
||||
"title": "Action"
|
||||
@@ -8207,6 +8305,7 @@
|
||||
"model_deleted",
|
||||
"email_sent",
|
||||
"update_available",
|
||||
"export_ready",
|
||||
"custom_"
|
||||
],
|
||||
"title": "Action",
|
||||
@@ -8299,6 +8398,7 @@
|
||||
"authentik.enterprise.providers.radius",
|
||||
"authentik.enterprise.providers.scim",
|
||||
"authentik.enterprise.providers.ssf",
|
||||
"authentik.enterprise.reports",
|
||||
"authentik.enterprise.search",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
"authentik.enterprise.stages.mtls",
|
||||
@@ -8424,6 +8524,7 @@
|
||||
"authentik_providers_microsoft_entra.microsoftentraprovider",
|
||||
"authentik_providers_microsoft_entra.microsoftentraprovidermapping",
|
||||
"authentik_providers_ssf.ssfprovider",
|
||||
"authentik_reports.dataexport",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
||||
"authentik_stages_mtls.mutualtlsstage",
|
||||
"authentik_stages_source.sourcestage"
|
||||
@@ -10945,6 +11046,10 @@
|
||||
"authentik_rbac.view_role",
|
||||
"authentik_rbac.view_system_info",
|
||||
"authentik_rbac.view_system_settings",
|
||||
"authentik_reports.add_dataexport",
|
||||
"authentik_reports.change_dataexport",
|
||||
"authentik_reports.delete_dataexport",
|
||||
"authentik_reports.view_dataexport",
|
||||
"authentik_sources_kerberos.add_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.add_kerberossource",
|
||||
"authentik_sources_kerberos.add_kerberossourcepropertymapping",
|
||||
|
||||
+425
@@ -4499,6 +4499,145 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/users/export/:
|
||||
post:
|
||||
operationId: core_users_export_create
|
||||
description: |-
|
||||
Create a data export for this data type. Note that the export is generated asynchronously:
|
||||
this method returns a `DataExport` object that will initially have `completed=false` as well
|
||||
as the permanent URL to that object in the `Location` header.
|
||||
You can poll that URL until `completed=true`, at which point the `file_url` property will
|
||||
contain a URL to download
|
||||
parameters:
|
||||
- in: query
|
||||
name: attributes
|
||||
schema:
|
||||
type: string
|
||||
description: Attributes
|
||||
- in: query
|
||||
name: date_joined
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: date_joined__gt
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: date_joined__lt
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: email
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: groups_by_name
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: groups_by_pk
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: is_active
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: is_superuser
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: last_updated
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: last_updated__gt
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: last_updated__lt
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- $ref: '#/components/parameters/QueryName'
|
||||
- $ref: '#/components/parameters/QueryPaginationOrdering'
|
||||
- in: query
|
||||
name: path
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: path_startswith
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: roles_by_name
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: roles_by_pk
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
explode: true
|
||||
style: form
|
||||
- $ref: '#/components/parameters/QuerySearch'
|
||||
- in: query
|
||||
name: type
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- external
|
||||
- internal
|
||||
- internal_service_account
|
||||
- service_account
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: username
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: uuid
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DataExport'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/users/impersonate_end/:
|
||||
get:
|
||||
operationId: core_users_impersonate_end_retrieve
|
||||
@@ -6510,6 +6649,7 @@ paths:
|
||||
- configuration_error
|
||||
- custom_
|
||||
- email_sent
|
||||
- export_ready
|
||||
- flow_execution
|
||||
- impersonation_ended
|
||||
- impersonation_started
|
||||
@@ -6745,6 +6885,108 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/events/events/export/:
|
||||
post:
|
||||
operationId: events_events_export_create
|
||||
description: |-
|
||||
Create a data export for this data type. Note that the export is generated asynchronously:
|
||||
this method returns a `DataExport` object that will initially have `completed=false` as well
|
||||
as the permanent URL to that object in the `Location` header.
|
||||
You can poll that URL until `completed=true`, at which point the `file_url` property will
|
||||
contain a URL to download
|
||||
parameters:
|
||||
- in: query
|
||||
name: action
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: actions
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- authorize_application
|
||||
- configuration_error
|
||||
- custom_
|
||||
- email_sent
|
||||
- export_ready
|
||||
- flow_execution
|
||||
- impersonation_ended
|
||||
- impersonation_started
|
||||
- invitation_used
|
||||
- login
|
||||
- login_failed
|
||||
- logout
|
||||
- model_created
|
||||
- model_deleted
|
||||
- model_updated
|
||||
- password_set
|
||||
- policy_exception
|
||||
- policy_execution
|
||||
- property_mapping_exception
|
||||
- secret_rotate
|
||||
- secret_view
|
||||
- source_linked
|
||||
- suspicious_request
|
||||
- system_exception
|
||||
- system_task_exception
|
||||
- system_task_execution
|
||||
- update_available
|
||||
- user_write
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: brand_name
|
||||
schema:
|
||||
type: string
|
||||
description: Brand name
|
||||
- in: query
|
||||
name: client_ip
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: context_authorized_app
|
||||
schema:
|
||||
type: string
|
||||
description: Context Authorized application
|
||||
- in: query
|
||||
name: context_model_app
|
||||
schema:
|
||||
type: string
|
||||
description: Context Model App
|
||||
- in: query
|
||||
name: context_model_name
|
||||
schema:
|
||||
type: string
|
||||
description: Context Model Name
|
||||
- in: query
|
||||
name: context_model_pk
|
||||
schema:
|
||||
type: string
|
||||
description: Context Model Primary Key
|
||||
- $ref: '#/components/parameters/QueryPaginationOrdering'
|
||||
- $ref: '#/components/parameters/QuerySearch'
|
||||
- in: query
|
||||
name: username
|
||||
schema:
|
||||
type: string
|
||||
description: Username
|
||||
tags:
|
||||
- events
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DataExport'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/events/events/top_per_user/:
|
||||
get:
|
||||
operationId: events_events_top_per_user_list
|
||||
@@ -6795,6 +7037,7 @@ paths:
|
||||
- configuration_error
|
||||
- custom_
|
||||
- email_sent
|
||||
- export_ready
|
||||
- flow_execution
|
||||
- impersonation_ended
|
||||
- impersonation_started
|
||||
@@ -10454,6 +10697,7 @@ paths:
|
||||
- configuration_error
|
||||
- custom_
|
||||
- email_sent
|
||||
- export_ready
|
||||
- flow_execution
|
||||
- impersonation_ended
|
||||
- impersonation_started
|
||||
@@ -19617,6 +19861,7 @@ paths:
|
||||
- authentik_providers_ssf.ssfprovider
|
||||
- authentik_rbac.initialpermissions
|
||||
- authentik_rbac.role
|
||||
- authentik_reports.dataexport
|
||||
- authentik_sources_kerberos.groupkerberossourceconnection
|
||||
- authentik_sources_kerberos.kerberossource
|
||||
- authentik_sources_kerberos.kerberossourcepropertymapping
|
||||
@@ -20069,6 +20314,76 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/reports/exports/:
|
||||
get:
|
||||
operationId: reports_exports_list
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/QueryPaginationOrdering'
|
||||
- $ref: '#/components/parameters/QueryPaginationPage'
|
||||
- $ref: '#/components/parameters/QueryPaginationPageSize'
|
||||
- $ref: '#/components/parameters/QuerySearch'
|
||||
tags:
|
||||
- reports
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedDataExportList'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/reports/exports/{id}/:
|
||||
get:
|
||||
operationId: reports_exports_retrieve
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Data Export.
|
||||
required: true
|
||||
tags:
|
||||
- reports
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DataExport'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
delete:
|
||||
operationId: reports_exports_destroy
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this Data Export.
|
||||
required: true
|
||||
tags:
|
||||
- reports
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: No response body
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/root/config/:
|
||||
get:
|
||||
operationId: root_config_retrieve
|
||||
@@ -32687,6 +33002,7 @@ components:
|
||||
- authentik.enterprise.providers.radius
|
||||
- authentik.enterprise.providers.scim
|
||||
- authentik.enterprise.providers.ssf
|
||||
- authentik.enterprise.reports
|
||||
- authentik.enterprise.search
|
||||
- authentik.enterprise.stages.authenticator_endpoint_gdtc
|
||||
- authentik.enterprise.stages.mtls
|
||||
@@ -35037,6 +35353,22 @@ components:
|
||||
description: 'Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3).'
|
||||
required:
|
||||
- name
|
||||
ContentType:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
readOnly: true
|
||||
app_label:
|
||||
type: string
|
||||
readOnly: true
|
||||
model:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- app_label
|
||||
- id
|
||||
- model
|
||||
ContextualFlowInfo:
|
||||
type: object
|
||||
description: Contextual flow information for a challenge
|
||||
@@ -35372,6 +35704,45 @@ components:
|
||||
- matched_domain
|
||||
- ui_footer_links
|
||||
- ui_theme
|
||||
DataExport:
|
||||
type: object
|
||||
description: |-
|
||||
Mixin to validate that a valid enterprise license
|
||||
exists before allowing to save the object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
requested_by:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RequestedBy'
|
||||
readOnly: true
|
||||
requested_on:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
content_type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ContentType'
|
||||
readOnly: true
|
||||
query_params:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
file_url:
|
||||
type: string
|
||||
readOnly: true
|
||||
completed:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
required:
|
||||
- completed
|
||||
- content_type
|
||||
- file_url
|
||||
- id
|
||||
- query_params
|
||||
- requested_by
|
||||
- requested_on
|
||||
DeliveryMethodEnum:
|
||||
enum:
|
||||
- https://schemas.openid.net/secevent/risc/delivery-method/push
|
||||
@@ -36970,6 +37341,7 @@ components:
|
||||
- model_deleted
|
||||
- email_sent
|
||||
- update_available
|
||||
- export_ready
|
||||
- custom_
|
||||
type: string
|
||||
EventMatcherPolicy:
|
||||
@@ -41054,6 +41426,7 @@ components:
|
||||
- authentik_providers_microsoft_entra.microsoftentraprovider
|
||||
- authentik_providers_microsoft_entra.microsoftentraprovidermapping
|
||||
- authentik_providers_ssf.ssfprovider
|
||||
- authentik_reports.dataexport
|
||||
- authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage
|
||||
- authentik_stages_mtls.mutualtlsstage
|
||||
- authentik_stages_source.sourcestage
|
||||
@@ -41284,6 +41657,13 @@ components:
|
||||
body:
|
||||
type: string
|
||||
readOnly: true
|
||||
hyperlink:
|
||||
type: string
|
||||
nullable: true
|
||||
maxLength: 4096
|
||||
hyperlink_label:
|
||||
type: string
|
||||
nullable: true
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -41301,6 +41681,13 @@ components:
|
||||
type: object
|
||||
description: Notification Serializer
|
||||
properties:
|
||||
hyperlink:
|
||||
type: string
|
||||
nullable: true
|
||||
maxLength: 4096
|
||||
hyperlink_label:
|
||||
type: string
|
||||
nullable: true
|
||||
event:
|
||||
$ref: '#/components/schemas/EventRequest'
|
||||
seen:
|
||||
@@ -42727,6 +43114,21 @@ components:
|
||||
- pagination
|
||||
- results
|
||||
- autocomplete
|
||||
PaginatedDataExportList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DataExport'
|
||||
autocomplete:
|
||||
$ref: '#/components/schemas/Autocomplete'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
- autocomplete
|
||||
PaginatedDenyStageList:
|
||||
type: object
|
||||
properties:
|
||||
@@ -46937,6 +47339,13 @@ components:
|
||||
type: object
|
||||
description: Notification Serializer
|
||||
properties:
|
||||
hyperlink:
|
||||
type: string
|
||||
nullable: true
|
||||
maxLength: 4096
|
||||
hyperlink_label:
|
||||
type: string
|
||||
nullable: true
|
||||
event:
|
||||
$ref: '#/components/schemas/EventRequest'
|
||||
seen:
|
||||
@@ -50920,6 +51329,22 @@ components:
|
||||
minimum: -2147483648
|
||||
required:
|
||||
- name
|
||||
RequestedBy:
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
readOnly: true
|
||||
title: ID
|
||||
username:
|
||||
type: string
|
||||
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
|
||||
only.
|
||||
pattern: ^[\w.@+-]+$
|
||||
maxLength: 150
|
||||
required:
|
||||
- pk
|
||||
- username
|
||||
ResidentKeyRequirementEnum:
|
||||
enum:
|
||||
- discouraged
|
||||
|
||||
@@ -72,7 +72,8 @@ export const createAdminSidebarEntries = (): readonly SidebarEntry[] => [
|
||||
[null, msg("Events"), null, [
|
||||
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
|
||||
["/events/rules", msg("Notification Rules")],
|
||||
["/events/transports", msg("Notification Transports")]]
|
||||
["/events/transports", msg("Notification Transports")],
|
||||
["/events/exports", msg("Data Exports"), {enterprise:true}]]
|
||||
],
|
||||
[null, msg("Customization"), null, [
|
||||
["/policy/policies", msg("Policies")],
|
||||
|
||||
@@ -153,6 +153,10 @@ export const ROUTES: Route[] = [
|
||||
await import("#admin/events/RuleListPage");
|
||||
return html`<ak-event-rule-list></ak-event-rule-list>`;
|
||||
}),
|
||||
new Route(new RegExp("^/events/exports"), async () => {
|
||||
await import("./events/DataExportListPage");
|
||||
return html`<ak-data-export-list></ak-data-export-list>`;
|
||||
}),
|
||||
new Route(new RegExp("^/outpost/outposts$"), async () => {
|
||||
await import("#admin/outposts/OutpostListPage");
|
||||
return html`<ak-outpost-list></ak-outpost-list>`;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#elements/buttons/ActionButton/index";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/tasks/TaskList";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { TablePage } from "#elements/table/TablePage";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { DataExport, ReportsApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-data-export-list")
|
||||
export class DataExportListPage extends TablePage<DataExport> {
|
||||
protected override searchEnabled = true;
|
||||
public pageTitle = msg("Data Exports");
|
||||
public pageDescription = msg("Manage past data exports.");
|
||||
public pageIcon = "pf-icon pf-icon-export";
|
||||
|
||||
public override checkbox = true;
|
||||
public override clearOnRefresh = true;
|
||||
public override expandable = true;
|
||||
|
||||
@property({ type: String })
|
||||
public order = "-requested_on";
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<DataExport>> {
|
||||
return new ReportsApi(DEFAULT_CONFIG).reportsExportsList(
|
||||
await this.defaultEndpointConfig(),
|
||||
);
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
[msg("Data type"), "content_type__model"],
|
||||
[msg("Requested by"), "requested_by"],
|
||||
[msg("Creation date"), "requested_on"],
|
||||
[msg("Completed"), "completed"],
|
||||
[msg("Actions"), null, msg("Row actions")],
|
||||
];
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html` <ak-forms-delete-bulk
|
||||
objectLabel=${msg("Data export(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.delete=${(item: DataExport) => {
|
||||
return new ReportsApi(DEFAULT_CONFIG).reportsExportsDestroy({
|
||||
id: item.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${msg("Delete")}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: DataExport): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`${item.contentType.model}`,
|
||||
html`<a href="#/identity/users/${item.requestedBy.pk}"
|
||||
>${item.requestedBy.username}</a
|
||||
>`,
|
||||
Timestamp(item.requestedOn),
|
||||
html`${item.completed ? msg("Yes") : msg("No")}`,
|
||||
item.completed && item.fileUrl
|
||||
? html`<div>
|
||||
<a href="${item.fileUrl}">
|
||||
<pf-tooltip position="top" content=${msg("Download")}>
|
||||
<i class="fas fa-download" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</a>
|
||||
</div>`
|
||||
: html``,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: DataExport): TemplateResult {
|
||||
return html` <dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-card__title">${msg("Query parameters")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<code>${JSON.stringify(item.queryParams, null, 4)}</code>
|
||||
</div>
|
||||
</dl>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-data-export-list": DataExportListPage;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import "#admin/events/EventMap";
|
||||
import "#admin/events/EventVolumeChart";
|
||||
import "#admin/reports/ExportButton";
|
||||
import "#components/ak-event-info";
|
||||
import "#elements/Tabs";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
@@ -118,6 +119,19 @@ export class EventListPage extends WithLicenseSummary(TablePage<Event>) {
|
||||
renderExpanded(item: Event): TemplateResult {
|
||||
return html`<ak-event-info .event=${item as EventWithContext}></ak-event-info>`;
|
||||
}
|
||||
|
||||
protected renderToolbar(): TemplateResult {
|
||||
return html`${super.renderToolbar()}
|
||||
<ak-reports-export-button
|
||||
.createExport=${this.#createExport}
|
||||
></ak-reports-export-button>`;
|
||||
}
|
||||
|
||||
#createExport = async () => {
|
||||
await new EventsApi(DEFAULT_CONFIG).eventsEventsExportCreate({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { parseAPIResponseError } from "../../common/errors/network";
|
||||
import { MessageLevel } from "../../common/messages";
|
||||
import { AKElement } from "../../elements/Base";
|
||||
import { showAPIErrorMessage, showMessage } from "../../elements/messages/MessageContainer";
|
||||
import { SlottedTemplateResult } from "../../elements/types";
|
||||
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-reports-export-button")
|
||||
export class ExportButton extends WithLicenseSummary(AKElement) {
|
||||
static styles: CSSResult[] = [PFBase, PFButton];
|
||||
|
||||
@property({ attribute: false })
|
||||
// public createExport: (() => Promise<void>) | null = null;
|
||||
public createExport: (() => Promise<void>) | null = null;
|
||||
|
||||
// safest display setting for a button
|
||||
cachedDisplay = "inline-block";
|
||||
|
||||
// memoize what the button would be if it were visible:
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const detectedDisplay = getComputedStyle(this).display;
|
||||
if (detectedDisplay) {
|
||||
this.cachedDisplay = detectedDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
// Take it out of the DOM flow if it's not enabled
|
||||
willUpdate(changed: PropertyValues<this>) {
|
||||
super.willUpdate(changed);
|
||||
this.style.display = this.hasEnterpriseLicense ? this.cachedDisplay : "none";
|
||||
}
|
||||
|
||||
#clickHandler = () => {
|
||||
if (typeof this.createExport !== "function") {
|
||||
throw new TypeError("`createExport` property must be a function");
|
||||
}
|
||||
|
||||
return this.createExport()
|
||||
.then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: msg("Data export requested successfully"),
|
||||
description: msg("You will receive a notification once the data is ready"),
|
||||
});
|
||||
})
|
||||
.catch(async (error) => {
|
||||
const apiError = await parseAPIResponseError(error);
|
||||
showAPIErrorMessage(apiError);
|
||||
});
|
||||
};
|
||||
|
||||
render(): SlottedTemplateResult {
|
||||
if (!this.hasEnterpriseLicense) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<button @click=${this.#clickHandler} class="pf-c-button pf-m-secondary">
|
||||
${msg("Export")}
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-reports-export-button": ExportButton;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#admin/reports/ExportButton";
|
||||
import "#admin/users/ServiceAccountForm";
|
||||
import "#admin/users/UserActiveForm";
|
||||
import "#admin/users/UserForm";
|
||||
@@ -155,6 +156,14 @@ export class UserListPage extends WithBrandConfig(
|
||||
[msg("Actions"), null, msg("Row Actions")],
|
||||
];
|
||||
|
||||
#createExport = async () => {
|
||||
await new CoreApi(DEFAULT_CONFIG).coreUsersExportCreate({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
pathStartswith: this.activePath,
|
||||
isActive: this.hideDeactivated ? true : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
const { currentUser, originalUser } = this;
|
||||
@@ -393,6 +402,9 @@ export class UserListPage extends WithBrandConfig(
|
||||
${msg("New Service Account")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-reports-export-button
|
||||
.createExport=${this.#createExport}
|
||||
></ak-reports-export-button>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export const eventActionToLabel = new Map<EventActions | undefined, string>([
|
||||
[EventActions.ModelDeleted, msg("Model deleted")],
|
||||
[EventActions.EmailSent, msg("Email sent")],
|
||||
[EventActions.UpdateAvailable, msg("Update available")],
|
||||
[EventActions.ExportReady, msg("Data export ready")],
|
||||
]);
|
||||
|
||||
export const actionToLabel = (action?: EventActions): string =>
|
||||
|
||||
@@ -88,6 +88,14 @@ export class NotificationDrawer extends WithSession(AKElement) {
|
||||
});
|
||||
}
|
||||
|
||||
protected renderHyperlink(item: Notification) {
|
||||
if (!item.hyperlink) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<small><a href=${item.hyperlink}>${item.hyperlinkLabel}</a></small>`;
|
||||
}
|
||||
|
||||
#renderItem = (item: Notification): TemplateResult => {
|
||||
const label = actionToLabel(item.event?.action);
|
||||
const level = severityToLevel(item.severity);
|
||||
@@ -144,6 +152,7 @@ export class NotificationDrawer extends WithSession(AKElement) {
|
||||
${formatElapsedTime(item.created!)}
|
||||
</pf-tooltip></small
|
||||
>
|
||||
${this.renderHyperlink(item)}
|
||||
</li>`;
|
||||
};
|
||||
|
||||
|
||||
@@ -49,3 +49,12 @@ button.pf-c-nav__link {
|
||||
.pf-c-nav__item:not(.pf-m-expandable) {
|
||||
--pf-c-nav__link--after--BorderLeftWidth: 1px !important;
|
||||
}
|
||||
|
||||
.pf-c-nav__enterprise-notice {
|
||||
color: var(--pf-global--Color--dark-200);
|
||||
background-color: var(--pf-global--Color--light-300);
|
||||
font-size: var(--pf-global--FontSize--xs);
|
||||
margin-inline-start: 0.5rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: var(--pf-global--BorderRadius--sm);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import "#admin/common/ak-license-notice";
|
||||
|
||||
import { WithCapabilitiesConfig } from "../mixins/capabilities";
|
||||
import { WithLicenseSummary } from "../mixins/license";
|
||||
|
||||
import { ROUTE_SEPARATOR } from "#common/constants";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import Styles from "#elements/sidebar/SidebarItem.css";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import { CapabilitiesEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
@@ -16,10 +23,11 @@ export interface SidebarItemProperties {
|
||||
path?: string | null;
|
||||
activeWhen?: string[];
|
||||
expanded?: boolean | null;
|
||||
enterprise?: boolean;
|
||||
}
|
||||
|
||||
@customElement("ak-sidebar-item")
|
||||
export class SidebarItem extends AKElement {
|
||||
export class SidebarItem extends WithCapabilitiesConfig(WithLicenseSummary(AKElement)) {
|
||||
static styles: CSSResult[] = [
|
||||
// ---
|
||||
PFPage,
|
||||
@@ -49,6 +57,9 @@ export class SidebarItem extends AKElement {
|
||||
|
||||
public parent?: SidebarItem;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public enterprise = false;
|
||||
|
||||
public get childItems(): SidebarItem[] {
|
||||
const children = Array.from(this.querySelectorAll<SidebarItem>("ak-sidebar-item") || []);
|
||||
children.forEach((child) => (child.parent = this));
|
||||
@@ -199,7 +210,18 @@ export class SidebarItem extends AKElement {
|
||||
</li>`;
|
||||
}
|
||||
|
||||
renderEnterpriseRequired() {
|
||||
return html`<a href="#/enterprise/licenses" class="pf-c-nav__link">
|
||||
${this.label}
|
||||
<span class="pf-c-nav__enterprise-notice">${msg("Enterprise only")}</span>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
renderWithPath() {
|
||||
if (this.enterprise && !this.hasEnterpriseLicense) {
|
||||
if (!this.can(CapabilitiesEnum.IsEnterprise)) return nothing;
|
||||
else return this.renderEnterpriseRequired();
|
||||
}
|
||||
return html`
|
||||
<a
|
||||
part="link ${this.current ? "current" : ""}"
|
||||
|
||||
@@ -668,6 +668,7 @@ const items = [
|
||||
"sys-mgmt/certificates",
|
||||
"sys-mgmt/settings",
|
||||
"sys-mgmt/service-accounts",
|
||||
"sys-mgmt/data-exports",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Data Exports
|
||||
sidebar_label: Data Exports
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
authentik enterprise allows you to export user and event data in CSV format for backup or analysis purposes.
|
||||
|
||||
The content included in a data export matches that returned by the API endpoints for the respective object types.
|
||||
|
||||
For detailed instructions on exporting users and events, see [Export users](../users-sources/user/user_basic_operations.md#export-users-) and [Export events](events/logging-events.md#export-events-) respectively.
|
||||
|
||||
You can access past data exports from the **System Management** > **Data Exports** page. On this page you can view the query used for a specific export, search exports by data type and user, download completed exports and delete exports that you no longer need.
|
||||
|
||||
## Permissions
|
||||
|
||||
Creating or viewing a data export requires [view permission](../users-sources/access-control/permissions.md) on the object type being exported in addition to the respective permission for data exports.
|
||||
@@ -306,3 +306,7 @@ An email has been sent. Included is the email that was sent.
|
||||
### `update_available`
|
||||
|
||||
An update is available.
|
||||
|
||||
### `export_ready`
|
||||
|
||||
A data export has been generated.
|
||||
|
||||
@@ -59,3 +59,22 @@ For more examples, refer to the list of [Event actions](./event-actions.md) and
|
||||
2. If the list of operators does not appear in a drop-down menu you will need to manually enter it.
|
||||
3. For queries that include `user`, `brand`, or `context` you need to use a compound term such as `user.username` or `brand.name`.
|
||||
:::
|
||||
|
||||
## Export events :ak-enterprise
|
||||
|
||||
You can export your authentik instance's events to a CSV file. To generate a data export, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator and navigate to **Events > Logs**.
|
||||
2. Set a [search query](#tell-me-more) as well as the ordering, as needed. The data export will honor these settings.
|
||||
3. Click **Export** above the event list.
|
||||
4. Note that the export is processed in the background. Once the export is ready, you will receive a notification in the notification drawer.
|
||||
5. In the notification, click **Download**.
|
||||
|
||||
6. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
7. Navigate to **Events** > **Logs**.
|
||||
8. Set a [search query](#tell-me-more) as well as the ordering for the data export.
|
||||
9. Click **Export** above the event list.
|
||||
10. The export is processed in the background and after it's ready, you will receive a notification in the notification drawer.
|
||||
11. In the notification, click **Download**.
|
||||
|
||||
To review, download, or delete past data exports, navigate to **Events** > **Data Exports** in the Admin interface.
|
||||
|
||||
@@ -187,3 +187,16 @@ An Admin can globally enable or disable impersonation in the [System Settings](.
|
||||
|
||||
An Admin can also configure whether inputting a reason for impersonation is required in the [System Settings](../../sys-mgmt/settings.md#require-reason-for-impersonation).
|
||||
:::
|
||||
|
||||
## Export users :ak-enterprise
|
||||
|
||||
You can export your authentik instance's user data to a CSV file. To generate a data export, follow these steps:
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Directory** > **Users** and click **Export**.
|
||||
3. Set a [search query](#tell-me-more) as well as the ordering for the data export.
|
||||
4. Click **Export** above the event list.
|
||||
5. The export is processed in the background and after it's ready, you will receive a notification in the Admin interface's notification area.
|
||||
6. In the notification, click **Download**.
|
||||
|
||||
To review, download, or delete past data exports, navigate to **Events** > **Data Exports** in the Admin interface.
|
||||
|
||||
Reference in New Issue
Block a user