diff --git a/authentik/endpoints/api/device_connections.py b/authentik/endpoints/api/device_connections.py index 2d86d80505..7d110c4e75 100644 --- a/authentik/endpoints/api/device_connections.py +++ b/authentik/endpoints/api/device_connections.py @@ -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 diff --git a/authentik/endpoints/api/device_fact_snapshots.py b/authentik/endpoints/api/device_fact_snapshots.py index 1f2bb0db2a..e5b486f513 100644 --- a/authentik/endpoints/api/device_fact_snapshots.py +++ b/authentik/endpoints/api/device_fact_snapshots.py @@ -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}, diff --git a/authentik/endpoints/connectors/agent/controller.py b/authentik/endpoints/connectors/agent/controller.py index e6a959960c..e3005a0b3f 100644 --- a/authentik/endpoints/connectors/agent/controller.py +++ b/authentik/endpoints/connectors/agent/controller.py @@ -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 [] diff --git a/authentik/endpoints/controller.py b/authentik/endpoints/controller.py index f543cccf21..6e05d7fd5a 100644 --- a/authentik/endpoints/controller.py +++ b/authentik/endpoints/controller.py @@ -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 [] diff --git a/schema.yml b/schema.yml index 3485793db0..d44b4b5530 100644 --- a/schema.yml +++ b/schema.yml @@ -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. diff --git a/web/src/admin/endpoints/devices/DeviceViewPage.ts b/web/src/admin/endpoints/devices/DeviceViewPage.ts index ea3ed8f5ec..2fba489c5b 100644 --- a/web/src/admin/endpoints/devices/DeviceViewPage.ts +++ b/web/src/admin/endpoints/devices/DeviceViewPage.ts @@ -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 {