diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 2e95a1396e..7a560959e0 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -6,7 +6,6 @@ from typing import Any from django.contrib.auth import update_session_auth_hash from django.contrib.auth.models import Permission -from django.db.models.functions import ExtractHour from django.db.transaction import atomic from django.db.utils import IntegrityError from django.urls import reverse_lazy @@ -52,7 +51,6 @@ from rest_framework.validators import UniqueValidator from rest_framework.viewsets import ModelViewSet from structlog.stdlib import get_logger -from authentik.admin.api.metrics import CoordinateSerializer from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.brands.models import Brand from authentik.core.api.used_by import UsedByMixin @@ -317,53 +315,6 @@ class SessionUserSerializer(PassiveSerializer): original = UserSelfSerializer(required=False) -class UserMetricsSerializer(PassiveSerializer): - """User Metrics""" - - logins = SerializerMethodField() - logins_failed = SerializerMethodField() - authorizations = SerializerMethodField() - - @extend_schema_field(CoordinateSerializer(many=True)) - def get_logins(self, _): - """Get successful logins per 8 hours for the last 7 days""" - user = self.context["user"] - request = self.context["request"] - return ( - get_objects_for_user(request.user, "authentik_events.view_event").filter( - action=EventAction.LOGIN, user__pk=user.pk - ) - # 3 data points per day, so 8 hour spans - .get_events_per(timedelta(days=7), ExtractHour, 7 * 3) - ) - - @extend_schema_field(CoordinateSerializer(many=True)) - def get_logins_failed(self, _): - """Get failed logins per 8 hours for the last 7 days""" - user = self.context["user"] - request = self.context["request"] - return ( - get_objects_for_user(request.user, "authentik_events.view_event").filter( - action=EventAction.LOGIN_FAILED, context__username=user.username - ) - # 3 data points per day, so 8 hour spans - .get_events_per(timedelta(days=7), ExtractHour, 7 * 3) - ) - - @extend_schema_field(CoordinateSerializer(many=True)) - def get_authorizations(self, _): - """Get failed logins per 8 hours for the last 7 days""" - user = self.context["user"] - request = self.context["request"] - return ( - get_objects_for_user(request.user, "authentik_events.view_event").filter( - action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk - ) - # 3 data points per day, so 8 hour spans - .get_events_per(timedelta(days=7), ExtractHour, 7 * 3) - ) - - class UsersFilter(FilterSet): """Filter for users""" @@ -607,17 +558,6 @@ class UserViewSet(UsedByMixin, ModelViewSet): update_session_auth_hash(self.request, user) return Response(status=204) - @permission_required("authentik_core.view_user", ["authentik_events.view_event"]) - @extend_schema(responses={200: UserMetricsSerializer(many=False)}) - @action(detail=True, pagination_class=None, filter_backends=[]) - def metrics(self, request: Request, pk: int) -> Response: - """User metrics per 1h""" - user: User = self.get_object() - serializer = UserMetricsSerializer(instance={}) - serializer.context["user"] = user - serializer.context["request"] = request - return Response(serializer.data) - @permission_required("authentik_core.reset_user_password") @extend_schema( responses={ diff --git a/authentik/events/api/events.py b/authentik/events/api/events.py index 265bc066d7..e7747acee1 100644 --- a/authentik/events/api/events.py +++ b/authentik/events/api/events.py @@ -62,7 +62,7 @@ class EventsFilter(django_filters.FilterSet): """Filter for events""" username = django_filters.CharFilter( - field_name="user", lookup_expr="username", label="Username" + field_name="user", label="Username", method="filter_username" ) context_model_pk = django_filters.CharFilter( field_name="context", @@ -97,6 +97,9 @@ class EventsFilter(django_filters.FilterSet): label="Brand name", ) + def filter_username(self, queryset, name, value): + return queryset.filter(Q(user__username=value) | Q(context__username=value)) + def filter_context_model_pk(self, queryset, name, value): """Because we store the PK as UUID.hex, we need to remove the dashes that a client may send. We can't use a diff --git a/schema.yml b/schema.yml index de88bd07f8..ab8099823c 100644 --- a/schema.yml +++ b/schema.yml @@ -4109,42 +4109,6 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /core/applications/{slug}/metrics/: - get: - operationId: core_applications_metrics_list - description: Metrics for application logins - parameters: - - in: path - name: slug - schema: - type: string - description: Internal application name, used in URLs. - required: true - tags: - - core - security: - - authentik: [] - responses: - '200': - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Coordinate' - description: '' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - description: '' - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' /core/applications/{slug}/set_icon/: post: operationId: core_applications_set_icon_create @@ -6044,40 +6008,6 @@ paths: schema: $ref: '#/components/schemas/GenericError' description: '' - /core/users/{id}/metrics/: - get: - operationId: core_users_metrics_retrieve - description: User metrics per 1h - parameters: - - in: path - name: id - schema: - type: integer - description: A unique integer value identifying this User. - required: true - tags: - - core - security: - - authentik: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/UserMetrics' - description: '' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/ValidationError' - description: '' - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/GenericError' - description: '' /core/users/{id}/recovery/: post: operationId: core_users_recovery_create @@ -60771,29 +60701,6 @@ components: - username_link - username_deny type: string - UserMetrics: - type: object - description: User Metrics - properties: - logins: - type: array - items: - $ref: '#/components/schemas/Coordinate' - readOnly: true - logins_failed: - type: array - items: - $ref: '#/components/schemas/Coordinate' - readOnly: true - authorizations: - type: array - items: - $ref: '#/components/schemas/Coordinate' - readOnly: true - required: - - authorizations - - logins - - logins_failed UserOAuthSourceConnection: type: object description: User source connection diff --git a/web/src/admin/users/UserChart.ts b/web/src/admin/users/UserChart.ts index fb7c7fedf6..5be01b2a37 100644 --- a/web/src/admin/users/UserChart.ts +++ b/web/src/admin/users/UserChart.ts @@ -1,71 +1,58 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKChart } from "@goauthentik/elements/charts/Chart"; -import { ChartData, Tick } from "chart.js"; +import { ChartData } from "chart.js"; -import { msg, str } from "@lit/localize"; +import { msg } from "@lit/localize"; import { customElement, property } from "lit/decorators.js"; -import { CoreApi, UserMetrics } from "@goauthentik/api"; +import { EventActions, EventVolume, EventsApi } from "@goauthentik/api"; @customElement("ak-charts-user") -export class UserChart extends AKChart { - @property({ type: Number }) - userId?: number; +export class UserChart extends AKChart { + @property() + username?: string; - async apiRequest(): Promise { - return new CoreApi(DEFAULT_CONFIG).coreUsersMetricsRetrieve({ - id: this.userId || 0, + async apiRequest(): Promise { + return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({ + actions: [ + EventActions.Login, + EventActions.LoginFailed, + EventActions.AuthorizeApplication, + ], + username: this.username, }); } - timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string { - const valueStamp = ticks[index]; - const delta = Date.now() - valueStamp.value; - const ago = Math.round(delta / 1000 / 3600 / 24); - return msg(str`${ago} days ago`); - } - - getChartData(data: UserMetrics): ChartData { - return { - datasets: [ - { - label: msg("Failed Logins"), - backgroundColor: "rgba(201, 25, 11, .5)", - spanGaps: true, - data: - data.loginsFailed?.map((cord) => { - return { - x: cord.xCord || 0, - y: cord.yCord || 0, - }; - }) || [], - }, - { - label: msg("Successful Logins"), - backgroundColor: "rgba(189, 229, 184, .5)", - spanGaps: true, - data: - data.logins?.map((cord) => { - return { - x: cord.xCord || 0, - y: cord.yCord || 0, - }; - }) || [], - }, - { - label: msg("Application authorizations"), - backgroundColor: "rgba(43, 154, 243, .5)", - spanGaps: true, - data: - data.authorizations?.map((cord) => { - return { - x: cord.xCord || 0, - y: cord.yCord || 0, - }; - }) || [], - }, - ], - }; + getChartData(data: EventVolume[]): ChartData { + return this.eventVolume( + data, + new Map([ + [ + EventActions.LoginFailed, + { + label: msg("Failed Logins"), + backgroundColor: "rgba(201, 25, 11, .5)", + spanGaps: true, + }, + ], + [ + EventActions.Login, + { + label: msg("Successful Logins"), + backgroundColor: "rgba(189, 229, 184, .5)", + spanGaps: true, + }, + ], + [ + EventActions.AuthorizeApplication, + { + label: msg("Application authorizations"), + backgroundColor: "rgba(43, 154, 243, .5)", + spanGaps: true, + }, + ], + ]), + ); } } diff --git a/web/src/admin/users/UserViewPage.ts b/web/src/admin/users/UserViewPage.ts index 6eba148926..78e457ca06 100644 --- a/web/src/admin/users/UserViewPage.ts +++ b/web/src/admin/users/UserViewPage.ts @@ -389,7 +389,7 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) { ${msg("Actions over the last week (per 8 hours)")}
- +