endpoints: show agent version (#19239)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2026-01-08 20:01:10 +01:00
committed by GitHub
parent e5c25b4d37
commit 3e9b59cc13
6 changed files with 83 additions and 21 deletions
+12 -3
View File
@@ -3,7 +3,7 @@ from rest_framework.fields import SerializerMethodField
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.api.connectors import ConnectorSerializer
from authentik.endpoints.api.device_fact_snapshots import DeviceFactSnapshotSerializer
from authentik.endpoints.models import DeviceConnection
from authentik.endpoints.models import Connector, DeviceConnection, DeviceFactSnapshot
class DeviceConnectionSerializer(ModelSerializer):
@@ -12,10 +12,19 @@ class DeviceConnectionSerializer(ModelSerializer):
latest_snapshot = SerializerMethodField(allow_null=True)
def get_latest_snapshot(self, instance: DeviceConnection) -> DeviceFactSnapshotSerializer:
snapshot = instance.devicefactsnapshot_set.order_by("-created").first()
snapshot: DeviceFactSnapshot | None = instance.devicefactsnapshot_set.order_by(
"-created"
).first()
if not snapshot:
return None
return DeviceFactSnapshotSerializer(snapshot).data
connector: Connector = Connector.objects.get_subclass(pk=snapshot.connection.connector_id)
vendor = connector.controller.vendor_identifier()
return DeviceFactSnapshotSerializer(
snapshot,
context={
"vendor": vendor,
},
).data
class Meta:
model = DeviceConnection
@@ -1,11 +1,32 @@
from enum import StrEnum
from rest_framework.fields import SerializerMethodField
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.controller import MERGED_VENDOR
from authentik.endpoints.facts import DeviceFacts
from authentik.endpoints.models import DeviceFactSnapshot
from authentik.endpoints.models import Connector, DeviceFactSnapshot
from authentik.lib.utils.reflection import all_subclasses
def get_vendor_choices():
choices = [(MERGED_VENDOR, MERGED_VENDOR)]
for connector_type in all_subclasses(Connector):
ident = connector_type().controller.vendor_identifier()
choices.append((ident, ident))
return choices
vendors = StrEnum("DeviceConnectorVendors", get_vendor_choices())
class DeviceFactSnapshotSerializer(ModelSerializer):
data = DeviceFacts()
vendor = SerializerMethodField()
def get_vendor(self, instance: DeviceFactSnapshot) -> vendors:
return self.context.get("vendor", MERGED_VENDOR)
class Meta:
model = DeviceFactSnapshot
@@ -14,6 +35,7 @@ class DeviceFactSnapshotSerializer(ModelSerializer):
"connection",
"created",
"expires",
"vendor",
]
extra_kwargs = {
"created": {"read_only": True},
@@ -44,6 +44,10 @@ class MDMConfigResponseSerializer(PassiveSerializer):
class AgentConnectorController(BaseController[AgentConnector]):
@staticmethod
def vendor_identifier() -> str:
return "goauthentik.io/platform"
def supported_enrollment_methods(self):
return []
+6
View File
@@ -5,6 +5,8 @@ from authentik.endpoints.models import Connector
from authentik.flows.stage import StageView
from authentik.lib.sentry import SentryIgnoredException
MERGED_VENDOR = "goauthentik.io/@merged"
class EnrollmentMethods(models.TextChoices):
# Automatically enrolled through user action
@@ -28,6 +30,10 @@ class BaseController[T: "Connector"]:
self.connector = connector
self.logger = get_logger().bind(connector=connector.name)
@staticmethod
def vendor_identifier() -> str:
raise NotImplementedError
def supported_enrollment_methods(self) -> list[EnrollmentMethods]:
return []
+10
View File
@@ -36086,11 +36086,16 @@ components:
format: date-time
readOnly: true
nullable: true
vendor:
allOf:
- $ref: '#/components/schemas/VendorEnum'
readOnly: true
required:
- connection
- created
- data
- expires
- vendor
DeviceFacts:
type: object
properties:
@@ -55908,6 +55913,11 @@ components:
code:
type: string
additionalProperties: {}
VendorEnum:
enum:
- goauthentik.io/@merged
- goauthentik.io/platform
type: string
Version:
type: object
description: Get running and latest version.
@@ -14,11 +14,11 @@ import { AKElement } from "#elements/Base";
import { Timestamp } from "#elements/table/shared";
import { setPageDetails } from "#components/ak-page-navbar";
import renderDescriptionList from "#components/DescriptionList";
import renderDescriptionList, { DescriptionPair } from "#components/DescriptionList";
import { getSize } from "#admin/endpoints/devices/utils";
import { Disk, EndpointDeviceDetails, EndpointsApi } from "@goauthentik/api";
import { DeviceConnection, Disk, EndpointDeviceDetails, EndpointsApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { CSSResult, html, nothing, PropertyValues } from "lit";
@@ -188,24 +188,26 @@ export class DeviceViewPage extends AKElement {
<div class="pf-l-grid__item pf-m-4-col pf-c-card">
<div class="pf-c-card__title">${msg("Connections")}</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-horizontal">
${this.device.connectionsObj.map((conn) => {
return html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${conn.connectorObj.name}</span
>
</dt>
<dd class="pf-c-description-list__description">
${renderDescriptionList(
this.device.connectionsObj.map((conn) => {
return [
html`${conn.connectorObj.name}`,
html`<div class="pf-c-description-list__text">
${msg(
str`Agent version: ${this.agentVersion(conn) ?? "-"}`,
)}
</div>
<div class="pf-c-description-list__text">
${conn.latestSnapshot?.created
? Timestamp(conn.latestSnapshot.created)
: html`-`}
</div>
</dd>
</div>`;
})}
</dl>
: nothing}
</div>`,
];
}) as DescriptionPair[],
{
horizontal: true,
},
)}
</div>
</div>
<div class="pf-l-grid__item pf-m-12-col pf-c-card">
@@ -219,6 +221,15 @@ export class DeviceViewPage extends AKElement {
</div>`;
}
agentVersion(conn: DeviceConnection): string | undefined {
const vendorContainer = conn.latestSnapshot?.data.vendor;
if (!vendorContainer) return;
const vendorData = vendorContainer[conn.latestSnapshot.vendor];
if (!vendorData) return;
if (!("agent_version" in vendorData)) return;
return vendorData.agent_version;
}
renderProcesses() {
if (!this.device) {
return nothing;