admin: add authorisations metric (#3811)
add authorizations metric Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
4fc21c3cc3
commit
b06a3a8f9f
|
@ -23,6 +23,7 @@ class LoginMetricsSerializer(PassiveSerializer):
|
||||||
|
|
||||||
logins_per_1h = SerializerMethodField()
|
logins_per_1h = SerializerMethodField()
|
||||||
logins_failed_per_1h = SerializerMethodField()
|
logins_failed_per_1h = SerializerMethodField()
|
||||||
|
authorizations_per_1h = SerializerMethodField()
|
||||||
|
|
||||||
@extend_schema_field(CoordinateSerializer(many=True))
|
@extend_schema_field(CoordinateSerializer(many=True))
|
||||||
def get_logins_per_1h(self, _):
|
def get_logins_per_1h(self, _):
|
||||||
|
@ -44,6 +45,16 @@ class LoginMetricsSerializer(PassiveSerializer):
|
||||||
.get_events_per_hour()
|
.get_events_per_hour()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@extend_schema_field(CoordinateSerializer(many=True))
|
||||||
|
def get_authorizations_per_1h(self, _):
|
||||||
|
"""Get successful authorizations per hour for the last 24 hours"""
|
||||||
|
user = self.context["user"]
|
||||||
|
return (
|
||||||
|
get_objects_for_user(user, "authentik_events.view_event")
|
||||||
|
.filter(action=EventAction.AUTHORIZE_APPLICATION)
|
||||||
|
.get_events_per_hour()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdministrationMetricsViewSet(APIView):
|
class AdministrationMetricsViewSet(APIView):
|
||||||
"""Login Metrics per 1h"""
|
"""Login Metrics per 1h"""
|
||||||
|
|
|
@ -28967,7 +28967,13 @@ components:
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Coordinate'
|
$ref: '#/components/schemas/Coordinate'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
authorizations_per_1h:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Coordinate'
|
||||||
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
|
- authorizations_per_1h
|
||||||
- logins_failed_per_1h
|
- logins_failed_per_1h
|
||||||
- logins_per_1h
|
- logins_per_1h
|
||||||
LoginSource:
|
LoginSource:
|
||||||
|
|
|
@ -3,6 +3,7 @@ import "@goauthentik/admin/admin-overview/cards/AdminStatusCard";
|
||||||
import "@goauthentik/admin/admin-overview/cards/SystemStatusCard";
|
import "@goauthentik/admin/admin-overview/cards/SystemStatusCard";
|
||||||
import "@goauthentik/admin/admin-overview/cards/VersionStatusCard";
|
import "@goauthentik/admin/admin-overview/cards/VersionStatusCard";
|
||||||
import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard";
|
import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard";
|
||||||
|
import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart";
|
||||||
import "@goauthentik/admin/admin-overview/charts/FlowStatusChart";
|
import "@goauthentik/admin/admin-overview/charts/FlowStatusChart";
|
||||||
import "@goauthentik/admin/admin-overview/charts/GroupCountStatusChart";
|
import "@goauthentik/admin/admin-overview/charts/GroupCountStatusChart";
|
||||||
import "@goauthentik/admin/admin-overview/charts/LDAPSyncStatusChart";
|
import "@goauthentik/admin/admin-overview/charts/LDAPSyncStatusChart";
|
||||||
|
@ -13,7 +14,6 @@ import { me } from "@goauthentik/common/users";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import "@goauthentik/elements/PageHeader";
|
import "@goauthentik/elements/PageHeader";
|
||||||
import "@goauthentik/elements/cards/AggregatePromiseCard";
|
import "@goauthentik/elements/cards/AggregatePromiseCard";
|
||||||
import "@goauthentik/elements/charts/AdminLoginsChart";
|
|
||||||
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
|
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
|
||||||
|
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
|
@ -189,9 +189,9 @@ export class AdminOverviewPage extends AKElement {
|
||||||
>
|
>
|
||||||
<ak-aggregate-card
|
<ak-aggregate-card
|
||||||
icon="pf-icon pf-icon-server"
|
icon="pf-icon pf-icon-server"
|
||||||
header=${t`Logins over the last 24 hours`}
|
header=${t`Logins and authorizations over the last 24 hours`}
|
||||||
>
|
>
|
||||||
<ak-charts-admin-login></ak-charts-admin-login>
|
<ak-charts-admin-login-authorization></ak-charts-admin-login-authorization>
|
||||||
</ak-aggregate-card>
|
</ak-aggregate-card>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import { AKChart, RGBAColor } from "@goauthentik/elements/charts/Chart";
|
||||||
|
import { ChartData } from "chart.js";
|
||||||
|
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
import { customElement } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import { AdminApi, LoginMetrics } from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-charts-admin-login-authorization")
|
||||||
|
export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
|
||||||
|
apiRequest(): Promise<LoginMetrics> {
|
||||||
|
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
|
||||||
|
}
|
||||||
|
|
||||||
|
getChartData(data: LoginMetrics): ChartData {
|
||||||
|
return {
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: t`Authorizations`,
|
||||||
|
backgroundColor: new RGBAColor(43, 154, 243, 0.5).toString(),
|
||||||
|
borderColor: new RGBAColor(43, 154, 243, 1).toString(),
|
||||||
|
spanGaps: true,
|
||||||
|
fill: "origin",
|
||||||
|
cubicInterpolationMode: "monotone",
|
||||||
|
tension: 0.4,
|
||||||
|
data: data.authorizationsPer1h.map((cord) => {
|
||||||
|
return {
|
||||||
|
x: cord.xCord,
|
||||||
|
y: cord.yCord,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t`Failed Logins`,
|
||||||
|
backgroundColor: new RGBAColor(201, 24, 11, 0.5).toString(),
|
||||||
|
borderColor: new RGBAColor(201, 24, 11, 1).toString(),
|
||||||
|
spanGaps: true,
|
||||||
|
fill: "origin",
|
||||||
|
cubicInterpolationMode: "monotone",
|
||||||
|
tension: 0.4,
|
||||||
|
data: data.loginsFailedPer1h.map((cord) => {
|
||||||
|
return {
|
||||||
|
x: cord.xCord,
|
||||||
|
y: cord.yCord,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t`Successful Logins`,
|
||||||
|
backgroundColor: new RGBAColor(62, 134, 53, 0.5).toString(),
|
||||||
|
borderColor: new RGBAColor(62, 134, 53, 1).toString(),
|
||||||
|
spanGaps: true,
|
||||||
|
fill: "origin",
|
||||||
|
cubicInterpolationMode: "monotone",
|
||||||
|
tension: 0.4,
|
||||||
|
data: data.loginsPer1h.map((cord) => {
|
||||||
|
return {
|
||||||
|
x: cord.xCord,
|
||||||
|
y: cord.yCord,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|
||||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
|
|
||||||
import { ChartData } from "chart.js";
|
|
||||||
|
|
||||||
import { t } from "@lingui/macro";
|
|
||||||
|
|
||||||
import { customElement } from "lit/decorators.js";
|
|
||||||
|
|
||||||
import { AdminApi, LoginMetrics } from "@goauthentik/api";
|
|
||||||
|
|
||||||
@customElement("ak-charts-admin-login")
|
|
||||||
export class AdminLoginsChart extends AKChart<LoginMetrics> {
|
|
||||||
apiRequest(): Promise<LoginMetrics> {
|
|
||||||
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
|
|
||||||
}
|
|
||||||
|
|
||||||
getChartData(data: LoginMetrics): ChartData {
|
|
||||||
return {
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: t`Failed Logins`,
|
|
||||||
backgroundColor: "rgba(201, 25, 11, .5)",
|
|
||||||
spanGaps: true,
|
|
||||||
data:
|
|
||||||
data.loginsFailedPer1h?.map((cord) => {
|
|
||||||
return {
|
|
||||||
x: cord.xCord || 0,
|
|
||||||
y: cord.yCord || 0,
|
|
||||||
};
|
|
||||||
}) || [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t`Successful Logins`,
|
|
||||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
|
||||||
spanGaps: true,
|
|
||||||
data:
|
|
||||||
data.loginsPer1h?.map((cord) => {
|
|
||||||
return {
|
|
||||||
x: cord.xCord || 0,
|
|
||||||
y: cord.yCord || 0,
|
|
||||||
};
|
|
||||||
}) || [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,16 @@
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { Chart, ChartConfiguration, ChartData, ChartOptions, Plugin, Tick } from "chart.js";
|
import {
|
||||||
|
Chart,
|
||||||
|
ChartConfiguration,
|
||||||
|
ChartData,
|
||||||
|
ChartOptions,
|
||||||
|
Filler,
|
||||||
|
LineElement,
|
||||||
|
Plugin,
|
||||||
|
PointElement,
|
||||||
|
Tick,
|
||||||
|
} from "chart.js";
|
||||||
import { Legend, Tooltip } from "chart.js";
|
import { Legend, Tooltip } from "chart.js";
|
||||||
import { BarController, DoughnutController, LineController } from "chart.js";
|
import { BarController, DoughnutController, LineController } from "chart.js";
|
||||||
import { ArcElement, BarElement } from "chart.js";
|
import { ArcElement, BarElement } from "chart.js";
|
||||||
|
@ -14,12 +24,33 @@ import { property } from "lit/decorators.js";
|
||||||
|
|
||||||
Chart.register(Legend, Tooltip);
|
Chart.register(Legend, Tooltip);
|
||||||
Chart.register(LineController, BarController, DoughnutController);
|
Chart.register(LineController, BarController, DoughnutController);
|
||||||
Chart.register(ArcElement, BarElement);
|
Chart.register(ArcElement, BarElement, PointElement, LineElement);
|
||||||
Chart.register(TimeScale, LinearScale);
|
Chart.register(TimeScale, LinearScale, Filler);
|
||||||
|
|
||||||
export const FONT_COLOUR_DARK_MODE = "#fafafa";
|
export const FONT_COLOUR_DARK_MODE = "#fafafa";
|
||||||
export const FONT_COLOUR_LIGHT_MODE = "#151515";
|
export const FONT_COLOUR_LIGHT_MODE = "#151515";
|
||||||
|
|
||||||
|
export class RGBAColor {
|
||||||
|
constructor(public r: number, public g: number, public b: number, public a: number = 1) {}
|
||||||
|
toString(): string {
|
||||||
|
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorFromString(stringInput: string): RGBAColor {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < stringInput.length; i++) {
|
||||||
|
hash = stringInput.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
hash = hash & hash;
|
||||||
|
}
|
||||||
|
const rgb = [0, 0, 0];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 255;
|
||||||
|
rgb[i] = value;
|
||||||
|
}
|
||||||
|
return new RGBAColor(rgb[0], rgb[1], rgb[2]);
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class AKChart<T> extends AKElement {
|
export abstract class AKChart<T> extends AKElement {
|
||||||
abstract apiRequest(): Promise<T>;
|
abstract apiRequest(): Promise<T>;
|
||||||
abstract getChartData(data: T): ChartData;
|
abstract getChartData(data: T): ChartData;
|
||||||
|
|
Reference in New Issue