From e2cb1a8d0c85bfe743cd5457954080f99cf9f394 Mon Sep 17 00:00:00 2001 From: "Jens L." Date: Fri, 23 Jan 2026 21:40:28 +0100 Subject: [PATCH] endpoints: FleetDM connector (#18589) * enterprise/endpoints/connectors/fleet: init Signed-off-by: Jens Langhammer # Conflicts: # blueprints/schema.json # schema.yml * add ui Signed-off-by: Jens Langhammer * fix desc Signed-off-by: Jens Langhammer * add configurable headers Signed-off-by: Jens Langhammer * add tests Signed-off-by: Jens Langhammer * Address review feedback on FleetDM connector implementation (#18651) * Initial plan * Add public override modifiers to updated method Co-authored-by: GirlBossRush <592134+GirlBossRush@users.noreply.github.com> * Address additional feedback from PR #18589 Co-authored-by: GirlBossRush <592134+GirlBossRush@users.noreply.github.com> * Fix indentation in ak-switch-input component Co-authored-by: GirlBossRush <592134+GirlBossRush@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: GirlBossRush <592134+GirlBossRush@users.noreply.github.com> * fix permission model Signed-off-by: Jens Langhammer * add attributes to device access group Signed-off-by: Jens Langhammer * add option to map device team Signed-off-by: Jens Langhammer * cleanup Signed-off-by: Jens Langhammer * update schema Signed-off-by: Jens Langhammer * format Signed-off-by: Jens Langhammer * switch connector to grid, add icons Signed-off-by: Jens Langhammer * fix pagination Signed-off-by: Jens Langhammer * add software tab Signed-off-by: Jens Langhammer * fix pages in test Signed-off-by: Jens Langhammer * add more test devices Signed-off-by: Jens Langhammer * add fedora test machine Signed-off-by: Jens Langhammer * better formatting for OS version Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: GirlBossRush <592134+GirlBossRush@users.noreply.github.com> --- .../endpoints/api/device_access_group.py | 1 + .../endpoints/connectors/agent/models.py | 5 + authentik/endpoints/facts.py | 20 +- .../0004_deviceaccessgroup_attributes.py | 18 + authentik/endpoints/models.py | 2 +- .../endpoints/connectors/fleet/__init__.py | 0 .../endpoints/connectors/fleet/api.py | 37 ++ .../endpoints/connectors/fleet/apps.py | 12 + .../endpoints/connectors/fleet/controller.py | 206 +++++++++++ .../fleet/migrations/0001_initial.py | 53 +++ .../connectors/fleet/migrations/__init__.py | 0 .../endpoints/connectors/fleet/models.py | 56 +++ .../connectors/fleet/tests/__init__.py | 0 .../fleet/tests/fixtures/host_fedora.json | 54 +++ .../fleet/tests/fixtures/host_macos.json | 68 ++++ .../fleet/tests/fixtures/host_ubuntu.json | 68 ++++ .../fleet/tests/fixtures/host_windows.json | 68 ++++ .../connectors/fleet/tests/test_connector.py | 133 +++++++ .../endpoints/connectors/fleet/urls.py | 3 + authentik/enterprise/settings.py | 1 + blueprints/schema.json | 127 +++++++ schema.yml | 350 ++++++++++++++++++ web/authentik/connectors/fleet.svg | 8 + .../endpoints/connectors/ConnectorViewPage.ts | 5 + .../endpoints/connectors/ConnectorWizard.ts | 3 + .../connectors/ConnectorsListPage.ts | 1 + .../connectors/fleet/FleetConnectorForm.ts | 95 +++++ .../fleet/FleetConnectorViewPage.ts | 157 ++++++++ .../admin/endpoints/devices/DeviceViewPage.ts | 26 +- .../devices/facts/DeviceSoftwareTable.ts | 59 +++ .../elements/wizard/TypeCreateWizardPage.ts | 109 +++--- 31 files changed, 1685 insertions(+), 60 deletions(-) create mode 100644 authentik/endpoints/migrations/0004_deviceaccessgroup_attributes.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/__init__.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/api.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/apps.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/controller.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/migrations/0001_initial.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/migrations/__init__.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/models.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/tests/__init__.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_fedora.json create mode 100644 authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_macos.json create mode 100644 authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_ubuntu.json create mode 100644 authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_windows.json create mode 100644 authentik/enterprise/endpoints/connectors/fleet/tests/test_connector.py create mode 100644 authentik/enterprise/endpoints/connectors/fleet/urls.py create mode 100644 web/authentik/connectors/fleet.svg create mode 100644 web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts create mode 100644 web/src/admin/endpoints/connectors/fleet/FleetConnectorViewPage.ts create mode 100644 web/src/admin/endpoints/devices/facts/DeviceSoftwareTable.ts diff --git a/authentik/endpoints/api/device_access_group.py b/authentik/endpoints/api/device_access_group.py index 72dc209c37..29d64d1fd6 100644 --- a/authentik/endpoints/api/device_access_group.py +++ b/authentik/endpoints/api/device_access_group.py @@ -12,6 +12,7 @@ class DeviceAccessGroupSerializer(ModelSerializer): fields = [ "pbm_uuid", "name", + "attributes", ] diff --git a/authentik/endpoints/connectors/agent/models.py b/authentik/endpoints/connectors/agent/models.py index 5b3cb46cc7..b036c6cd74 100644 --- a/authentik/endpoints/connectors/agent/models.py +++ b/authentik/endpoints/connectors/agent/models.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from uuid import uuid4 from django.db import models +from django.templatetags.static import static from django.utils.translation import gettext_lazy as _ from rest_framework.serializers import Serializer @@ -51,6 +52,10 @@ class AgentConnector(Connector): ) challenge_trigger_check_in = models.BooleanField(default=False) + @property + def icon_url(self): + return static("icons/icon.svg") + @property def serializer(self) -> type[Serializer]: from authentik.endpoints.connectors.agent.api.connectors import ( diff --git a/authentik/endpoints/facts.py b/authentik/endpoints/facts.py index 7b11902104..f2905d2c03 100644 --- a/authentik/endpoints/facts.py +++ b/authentik/endpoints/facts.py @@ -1,4 +1,5 @@ from django.db.models import TextChoices +from django.utils.translation import gettext_lazy as _ from drf_spectacular.extensions import OpenApiSerializerFieldExtension from drf_spectacular.plumbing import build_basic_type from drf_spectacular.types import OpenApiTypes @@ -15,7 +16,6 @@ from authentik.core.api.utils import JSONDictField class BigIntegerFieldFix(OpenApiSerializerFieldExtension): - target_class = "authentik.endpoints.facts.BigIntegerField" def map_serializer_field(self, auto_schema, direction): @@ -46,9 +46,23 @@ class DiskSerializer(Serializer): class OperatingSystemSerializer(Serializer): + """For example: + {"family":"linux","name":"Ubuntu","version":"24.04.3 LTS (Noble Numbat)","arch":"amd64"} + {"family": "windows","name":"Server 2022 Datacenter","version":"10.0.20348.4405","arch":"amd64"} + {"family": "windows","name":"Server 2022 Datacenter","version":"10.0.20348.4405","arch":"amd64"} + {"family": "mac_os", "name": "", "version": "26.2", "arch": "arm64"} + """ + family = ChoiceField(OSFamily.choices, required=True) - name = CharField(required=False) - version = CharField(required=False) + name = CharField( + required=False, help_text=_("Operating System name, such as 'Server 2022' or 'Ubuntu'") + ) + version = CharField( + required=False, + help_text=_( + "Operating System version, must always be the version number but may contain build name" + ), + ) arch = CharField(required=True) diff --git a/authentik/endpoints/migrations/0004_deviceaccessgroup_attributes.py b/authentik/endpoints/migrations/0004_deviceaccessgroup_attributes.py new file mode 100644 index 0000000000..1d705ab1a0 --- /dev/null +++ b/authentik/endpoints/migrations/0004_deviceaccessgroup_attributes.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2025-12-08 23:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_endpoints", "0003_alter_endpointstage_options_endpointstage_mode"), + ] + + operations = [ + migrations.AddField( + model_name="deviceaccessgroup", + name="attributes", + field=models.JSONField(blank=True, default=dict), + ), + ] diff --git a/authentik/endpoints/models.py b/authentik/endpoints/models.py index fe3114d520..1cc04b5fb7 100644 --- a/authentik/endpoints/models.py +++ b/authentik/endpoints/models.py @@ -175,7 +175,7 @@ class Connector(ScheduledModel, SerializerModel): ] -class DeviceAccessGroup(SerializerModel, PolicyBindingModel): +class DeviceAccessGroup(AttributesMixin, SerializerModel, PolicyBindingModel): name = models.TextField(unique=True) diff --git a/authentik/enterprise/endpoints/connectors/fleet/__init__.py b/authentik/enterprise/endpoints/connectors/fleet/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/enterprise/endpoints/connectors/fleet/api.py b/authentik/enterprise/endpoints/connectors/fleet/api.py new file mode 100644 index 0000000000..90d11baa7a --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/api.py @@ -0,0 +1,37 @@ +"""FleetConnector API Views""" + +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.used_by import UsedByMixin +from authentik.endpoints.api.connectors import ConnectorSerializer +from authentik.enterprise.api import EnterpriseRequiredMixin +from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector + + +class FleetConnectorSerializer(EnterpriseRequiredMixin, ConnectorSerializer): + """FleetConnector Serializer""" + + class Meta(ConnectorSerializer.Meta): + model = FleetConnector + fields = ConnectorSerializer.Meta.fields + [ + "url", + "token", + "headers_mapping", + "map_users", + "map_teams_access_group", + ] + extra_kwargs = { + "token": {"write_only": True}, + } + + +class FleetConnectorViewSet(UsedByMixin, ModelViewSet): + """FleetConnector Viewset""" + + queryset = FleetConnector.objects.all() + serializer_class = FleetConnectorSerializer + filterset_fields = [ + "name", + ] + search_fields = ["name"] + ordering = ["name"] diff --git a/authentik/enterprise/endpoints/connectors/fleet/apps.py b/authentik/enterprise/endpoints/connectors/fleet/apps.py new file mode 100644 index 0000000000..f14f9cab78 --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/apps.py @@ -0,0 +1,12 @@ +"""authentik endpoints app config""" + +from authentik.enterprise.apps import EnterpriseConfig + + +class AuthentikEnterpriseEndpointsConnectorFleetAppConfig(EnterpriseConfig): + """authentik endpoints app config""" + + name = "authentik.enterprise.endpoints.connectors.fleet" + label = "authentik_endpoints_connectors_fleet" + verbose_name = "authentik Enterprise.Endpoints.Connectors.Fleet" + default = True diff --git a/authentik/enterprise/endpoints/connectors/fleet/controller.py b/authentik/enterprise/endpoints/connectors/fleet/controller.py new file mode 100644 index 0000000000..386bc8c3b2 --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/controller.py @@ -0,0 +1,206 @@ +import re +from typing import Any + +from django.db import transaction +from requests import RequestException +from rest_framework.exceptions import ValidationError + +from authentik.core.models import User +from authentik.endpoints.controller import BaseController, ConnectorSyncException, EnrollmentMethods +from authentik.endpoints.facts import ( + DeviceFacts, + OSFamily, +) +from authentik.endpoints.models import ( + Device, + DeviceAccessGroup, + DeviceConnection, + DeviceUserBinding, +) +from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector as DBC +from authentik.events.utils import sanitize_item +from authentik.lib.utils.http import get_http_session +from authentik.policies.utils import delete_none_values + + +class FleetController(BaseController[DBC]): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._session = get_http_session() + self._session.headers["Authorization"] = f"Bearer {self.connector.token}" + if self.connector.headers_mapping: + self._session.headers.update( + sanitize_item( + self.connector.headers_mapping.evaluate( + user=None, + request=None, + connector=self.connector, + ) + ) + ) + + @staticmethod + def vendor_identifier() -> str: + return "fleetdm.com" + + def supported_enrollment_methods(self) -> list[EnrollmentMethods]: + return [EnrollmentMethods.AUTOMATIC_API] + + def _url(self, path: str) -> str: + return f"{self.connector.url}{path}" + + def _paginate_hosts(self): + try: + page = 0 + while True: + self.logger.info("Fetching page of hosts...", page=page) + res = self._session.get( + self._url("/api/v1/fleet/hosts"), + params={ + "order_key": "hardware_serial", + "page": page, + "per_page": 50, + "device_mapping": "true", + "populate_software": "true", + "populate_users": "true", + }, + ) + res.raise_for_status() + hosts: list[dict[str, Any]] = res.json()["hosts"] + if len(hosts) < 1: + self.logger.info("No more hosts, finished") + break + self.logger.info("Got hosts", count=len(hosts)) + yield from hosts + page += 1 + except RequestException as exc: + raise ConnectorSyncException(exc) from exc + + @transaction.atomic + def sync_endpoints(self) -> None: + for host in self._paginate_hosts(): + serial = host["hardware_serial"] + device, _ = Device.objects.get_or_create( + identifier=serial, defaults={"name": host["hostname"], "expiring": False} + ) + connection, _ = DeviceConnection.objects.update_or_create( + device=device, + connector=self.connector, + ) + if self.connector.map_users: + self.map_users(host, device) + if self.connector.map_teams_access_group: + self.map_access_group(host, device) + try: + connection.create_snapshot(self.convert_host_data(host)) + except ValidationError as exc: + self.logger.warning( + "failed to create snapshot for host", host=host["hostname"], exc=exc + ) + + def map_users(self, host: dict[str, Any], device: Device): + for raw_user in host.get("device_mapping", []) or []: + user = User.objects.filter(email=raw_user["email"]).first() + if not user: + continue + DeviceUserBinding.objects.update_or_create( + target=device, + user=user, + create_defaults={ + "is_primary": True, + "order": 0, + }, + ) + + def map_access_group(self, host: dict[str, Any], device: Device): + team_name = host.get("team_name") + if not team_name: + return + group, _ = DeviceAccessGroup.objects.get_or_create(name=team_name) + group.attributes["io.goauthentik.endpoints.connectors.fleet.team_id"] = host["team_id"] + if device.access_group: + return + device.access_group = group + device.save() + + @staticmethod + def os_family(host: dict[str, Any]) -> OSFamily: + if host["platform_like"] in ["debian", "rhel"]: + return OSFamily.linux + if host["platform_like"] == "windows": + return OSFamily.windows + if host["platform_like"] == "darwin": + return OSFamily.macOS + if host["platform"] == "android": + return OSFamily.android + if host["platform"] in ["ipados", "ios"]: + return OSFamily.iOS + return OSFamily.other + + def map_os(self, host: dict[str, Any]) -> dict[str, str]: + family = FleetController.os_family(host) + os = { + "arch": self.or_none(host["cpu_type"]), + "family": family, + "name": self.or_none(host["platform_like"]), + "version": self.or_none(host["os_version"]), + } + if not host["os_version"]: + return delete_none_values(os) + version = re.search(r"(\d+\.(?:\d+\.?)+)", host["os_version"]) + if not version: + return delete_none_values(os) + os["version"] = host["os_version"][version.start() :].strip() + os["name"] = host["os_version"][0 : version.start()].strip() + return delete_none_values(os) + + def or_none(self, value) -> Any | None: + if value == "": + return None + return value + + def convert_host_data(self, host: dict[str, Any]) -> dict[str, Any]: + """Convert host data from fleet to authentik""" + fleet_version = "" + for pkg in host.get("software") or []: + if pkg["name"] in ["fleet-osquery", "fleet-desktop"]: + fleet_version = pkg["version"] + data = { + "os": self.map_os(host), + "disks": [], + "network": delete_none_values( + {"hostname": self.or_none(host["hostname"]), "interfaces": []} + ), + "hardware": delete_none_values( + { + "model": self.or_none(host["hardware_model"]), + "manufacturer": self.or_none(host["hardware_vendor"]), + "serial": self.or_none(host["hardware_serial"]), + "cpu_name": self.or_none(host["cpu_brand"]), + "cpu_count": self.or_none(host["cpu_logical_cores"]), + "memory_bytes": self.or_none(host["memory"]), + } + ), + "software": [ + delete_none_values( + { + "name": x["name"], + "version": x["version"], + "source": x["source"], + } + ) + for x in (host.get("software") or []) + ], + "vendor": { + "fleetdm.com": { + "policies": [ + delete_none_values({"name": policy["name"], "status": policy["response"]}) + for policy in host.get("policies", []) + ], + "agent_version": fleet_version, + }, + }, + } + facts = DeviceFacts(data=data) + facts.is_valid(raise_exception=True) + return facts.validated_data diff --git a/authentik/enterprise/endpoints/connectors/fleet/migrations/0001_initial.py b/authentik/enterprise/endpoints/connectors/fleet/migrations/0001_initial.py new file mode 100644 index 0000000000..8beabaa466 --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.10 on 2026-01-15 13:27 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("authentik_endpoints", "0004_deviceaccessgroup_attributes"), + ("authentik_events", "0014_notification_hyperlink_notification_hyperlink_label_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="FleetConnector", + fields=[ + ( + "connector_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_endpoints.connector", + ), + ), + ("url", models.URLField()), + ("token", models.TextField()), + ("map_users", models.BooleanField(default=True)), + ("map_teams_access_group", models.BooleanField(default=False)), + ( + "headers_mapping", + models.ForeignKey( + default=None, + help_text="Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs", + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + related_name="+", + to="authentik_events.notificationwebhookmapping", + ), + ), + ], + options={ + "verbose_name": "Fleet Connector", + "verbose_name_plural": "Fleet Connectors", + }, + bases=("authentik_endpoints.connector",), + ), + ] diff --git a/authentik/enterprise/endpoints/connectors/fleet/migrations/__init__.py b/authentik/enterprise/endpoints/connectors/fleet/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/enterprise/endpoints/connectors/fleet/models.py b/authentik/enterprise/endpoints/connectors/fleet/models.py new file mode 100644 index 0000000000..56fc0390d2 --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/models.py @@ -0,0 +1,56 @@ +from typing import TYPE_CHECKING + +from django.db import models +from django.templatetags.static import static +from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import Serializer + +from authentik.endpoints.models import Connector + +if TYPE_CHECKING: + from authentik.enterprise.endpoints.connectors.fleet.controller import FleetController + + +class FleetConnector(Connector): + """Ingest device data and policy compliance from a Fleet instance.""" + + url = models.URLField() + token = models.TextField() + headers_mapping = models.ForeignKey( + "authentik_events.NotificationWebhookMapping", + on_delete=models.SET_DEFAULT, + null=True, + default=None, + related_name="+", + help_text=_( + "Configure additional headers to be sent. " + "Mapping should return a dictionary of key-value pairs" + ), + ) + + map_users = models.BooleanField(default=True) + map_teams_access_group = models.BooleanField(default=False) + + @property + def icon_url(self): + return static("authentik/connectors/fleet.svg") + + @property + def serializer(self) -> type[Serializer]: + from authentik.enterprise.endpoints.connectors.fleet.api import FleetConnectorSerializer + + return FleetConnectorSerializer + + @property + def controller(self) -> type[FleetController]: + from authentik.enterprise.endpoints.connectors.fleet.controller import FleetController + + return FleetController + + @property + def component(self) -> str: + return "ak-endpoints-connector-fleet-form" + + class Meta: + verbose_name = _("Fleet Connector") + verbose_name_plural = _("Fleet Connectors") diff --git a/authentik/enterprise/endpoints/connectors/fleet/tests/__init__.py b/authentik/enterprise/endpoints/connectors/fleet/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_fedora.json b/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_fedora.json new file mode 100644 index 0000000000..6ceea2e14d --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_fedora.json @@ -0,0 +1,54 @@ +{ + "created_at": "2026-01-23T15:32:17Z", + "updated_at": "2026-01-23T15:32:28Z", + "software": null, + "software_updated_at": "2026-01-23T15:32:17Z", + "id": 16, + "detail_updated_at": "1970-01-02T00:00:00Z", + "label_updated_at": "1970-01-02T00:00:00Z", + "policy_updated_at": "1970-01-02T00:00:00Z", + "last_enrolled_at": "2026-01-23T15:32:19Z", + "seen_time": "2026-01-23T15:32:21Z", + "refetch_requested": true, + "hostname": "fedora-workstation", + "uuid": "578c4d56-aff8-0793-14ae-7947392f5fec", + "platform": "rhel", + "osquery_version": "5.21.0", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, + "os_version": "Fedora Linux 43.0.0", + "build": "", + "platform_like": "rhel", + "code_name": "", + "uptime": 0, + "memory": 4092518400, + "cpu_type": "x86_64", + "cpu_subtype": "165", + "cpu_brand": "Intel(R) Core(TM) i5-10500T CPU @ 2.30GHz", + "cpu_physical_cores": 2, + "cpu_logical_cores": 2, + "hardware_vendor": "VMware, Inc.", + "hardware_model": "VMware20,1", + "hardware_version": "None", + "hardware_serial": "VMware-56 4d 8c 57 f8 af 93 07-14 ae 79 47 39 2f 5f ec", + "computer_name": "fedora-workstation", + "public_ip": "", + "primary_ip": "", + "primary_mac": "", + "distributed_interval": 10, + "config_tls_refresh": 0, + "logger_tls_period": 10, + "team_id": 2, + "pack_stats": null, + "team_name": "prod", + "gigs_disk_space_available": 0, + "percent_disk_space_available": 0, + "gigs_total_disk_space": 0, + "gigs_all_disk_space": null, + "issues": { + "failing_policies_count": 0, + "critical_vulnerabilities_count": 0, + "total_issues_count": 0 + } +} diff --git a/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_macos.json b/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_macos.json new file mode 100644 index 0000000000..fad990794a --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_macos.json @@ -0,0 +1,68 @@ +{ + "created_at": "2025-06-25T22:21:35Z", + "updated_at": "2025-12-20T11:42:09Z", + "software": null, + "software_updated_at": "2025-10-22T02:24:25Z", + "id": 1, + "detail_updated_at": "2025-10-23T23:30:31Z", + "label_updated_at": "2025-10-23T23:30:31Z", + "policy_updated_at": "2025-10-23T23:02:11Z", + "last_enrolled_at": "2025-06-25T22:21:37Z", + "seen_time": "2025-10-23T23:59:08Z", + "refetch_requested": false, + "hostname": "jens-mac-vm.local", + "uuid": "C8B98348-A0A6-5838-A321-57B59D788269", + "platform": "darwin", + "osquery_version": "5.19.0", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, + "os_version": "macOS 26.0.1", + "build": "25A362", + "platform_like": "darwin", + "code_name": "", + "uptime": 256356000000000, + "memory": 4294967296, + "cpu_type": "arm64e", + "cpu_subtype": "ARM64E", + "cpu_brand": "Apple M1 Pro (Virtual)", + "cpu_physical_cores": 8, + "cpu_logical_cores": 8, + "hardware_vendor": "Apple Inc.", + "hardware_model": "VirtualMac2,1", + "hardware_version": "", + "hardware_serial": "Z5DDF07GK6", + "computer_name": "jens-mac-vm", + "public_ip": "92.116.179.252", + "primary_ip": "192.168.85.3", + "primary_mac": "e6:9d:21:c2:2f:19", + "distributed_interval": 10, + "config_tls_refresh": 60, + "logger_tls_period": 10, + "team_id": 2, + "pack_stats": null, + "team_name": "prod", + "gigs_disk_space_available": 23.82, + "percent_disk_space_available": 37, + "gigs_total_disk_space": 62.83, + "gigs_all_disk_space": null, + "issues": { + "failing_policies_count": 1, + "critical_vulnerabilities_count": 2, + "total_issues_count": 3 + }, + "device_mapping": null, + "mdm": { + "enrollment_status": "On (manual)", + "dep_profile_error": false, + "server_url": "https://fleet.beryjuio-home.k8s.beryju.io/mdm/apple/mdm", + "name": "Fleet", + "encryption_key_available": false, + "connected_to_fleet": true + }, + "refetch_critical_queries_until": null, + "last_restarted_at": "2025-10-21T00:17:55Z", + "status": "offline", + "display_text": "jens-mac-vm.local", + "display_name": "jens-mac-vm" +} diff --git a/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_ubuntu.json b/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_ubuntu.json new file mode 100644 index 0000000000..22e90f2c0b --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_ubuntu.json @@ -0,0 +1,68 @@ +{ + "created_at": "2025-11-01T17:25:34Z", + "updated_at": "2026-01-23T12:58:55Z", + "software": null, + "software_updated_at": "2026-01-23T12:58:55Z", + "id": 14, + "detail_updated_at": "2026-01-23T12:58:55Z", + "label_updated_at": "2026-01-23T12:58:55Z", + "policy_updated_at": "2026-01-23T12:29:58Z", + "last_enrolled_at": "2025-11-01T17:25:38Z", + "seen_time": "2026-01-23T13:17:27Z", + "refetch_requested": false, + "hostname": "ubuntu-desktop", + "uuid": "5a4a4d56-22b0-d77b-9ba5-0bdc8ff23b60", + "platform": "ubuntu", + "osquery_version": "5.21.0", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, + "os_version": "Ubuntu 24.04.3 LTS", + "build": "", + "platform_like": "debian", + "code_name": "noble", + "uptime": 1631433000000000, + "memory": 2062721024, + "cpu_type": "x86_64", + "cpu_subtype": "165", + "cpu_brand": "Intel(R) Core(TM) i5-10500T CPU @ 2.30GHz", + "cpu_physical_cores": 2, + "cpu_logical_cores": 2, + "hardware_vendor": "VMware, Inc.", + "hardware_model": "VMware20,1", + "hardware_version": "None", + "hardware_serial": "VMware-56 4d 4a 5a b0 22 7b d7-9b a5 0b dc 8f f2 3b 60", + "computer_name": "ubuntu-desktop", + "public_ip": "92.116.178.120", + "primary_ip": "10.120.20.61", + "primary_mac": "00:0c:29:f2:3b:60", + "distributed_interval": 10, + "config_tls_refresh": 60, + "logger_tls_period": 10, + "team_id": 2, + "pack_stats": null, + "team_name": "prod", + "gigs_disk_space_available": 7.37, + "percent_disk_space_available": 31, + "gigs_total_disk_space": 23.08, + "gigs_all_disk_space": 23.08, + "issues": { + "failing_policies_count": 0, + "critical_vulnerabilities_count": 0, + "total_issues_count": 0 + }, + "device_mapping": null, + "mdm": { + "enrollment_status": null, + "dep_profile_error": false, + "server_url": null, + "name": "", + "encryption_key_available": false, + "connected_to_fleet": false + }, + "refetch_critical_queries_until": null, + "last_restarted_at": "2026-01-04T15:48:22.390118Z", + "status": "online", + "display_text": "ubuntu-desktop", + "display_name": "ubuntu-desktop" +} diff --git a/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_windows.json b/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_windows.json new file mode 100644 index 0000000000..1d6e3e114f --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/tests/fixtures/host_windows.json @@ -0,0 +1,68 @@ +{ + "created_at": "2025-10-19T12:44:09Z", + "updated_at": "2026-01-23T13:11:45Z", + "software": null, + "software_updated_at": "2026-01-22T06:57:30Z", + "id": 13, + "detail_updated_at": "2026-01-23T12:51:35Z", + "label_updated_at": "2026-01-23T12:51:35Z", + "policy_updated_at": "2026-01-23T13:11:45Z", + "last_enrolled_at": "2025-11-05T20:27:14Z", + "seen_time": "2026-01-23T13:17:33Z", + "refetch_requested": false, + "hostname": "windows-server", + "uuid": "CFF12F42-9F7D-A575-2C48-01BDC6A733FB", + "platform": "windows", + "osquery_version": "5.21.0", + "orbit_version": null, + "fleet_desktop_version": null, + "scripts_enabled": null, + "os_version": "Windows Server 2022 Datacenter 21H2 10.0.20348.4405", + "build": "20348", + "platform_like": "windows", + "code_name": "Microsoft Windows Server 2022 Datacenter", + "uptime": 217075000000000, + "memory": 4294967296, + "cpu_type": "x86_64", + "cpu_subtype": "-1", + "cpu_brand": "Intel(R) Core(TM) i5-10500T CPU @ 2.30GHz", + "cpu_physical_cores": 1, + "cpu_logical_cores": 2, + "hardware_vendor": "VMware, Inc.", + "hardware_model": "VMware20,1", + "hardware_version": "-1", + "hardware_serial": "VMware-42 2f f1 cf 7d 9f 75 a5-2c 48 01 bd c6 a7 33 fb", + "computer_name": "WINDOWS-SERVER", + "public_ip": "92.116.178.120", + "primary_ip": "10.120.20.78", + "primary_mac": "00:50:56:af:fb:3a", + "distributed_interval": 10, + "config_tls_refresh": 60, + "logger_tls_period": 10, + "team_id": 2, + "pack_stats": null, + "team_name": "prod", + "gigs_disk_space_available": 68, + "percent_disk_space_available": 71, + "gigs_total_disk_space": 96, + "gigs_all_disk_space": null, + "issues": { + "failing_policies_count": 0, + "critical_vulnerabilities_count": 5, + "total_issues_count": 5 + }, + "device_mapping": null, + "mdm": { + "enrollment_status": null, + "dep_profile_error": false, + "server_url": null, + "name": "", + "encryption_key_available": false, + "connected_to_fleet": false + }, + "refetch_critical_queries_until": null, + "last_restarted_at": "2026-01-21T00:33:38.178036Z", + "status": "online", + "display_text": "windows-server", + "display_name": "WINDOWS-SERVER" +} diff --git a/authentik/enterprise/endpoints/connectors/fleet/tests/test_connector.py b/authentik/enterprise/endpoints/connectors/fleet/tests/test_connector.py new file mode 100644 index 0000000000..8cf03c2494 --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/tests/test_connector.py @@ -0,0 +1,133 @@ +from json import loads + +from requests_mock import Mocker +from rest_framework.test import APITestCase + +from authentik.endpoints.facts import OSFamily +from authentik.endpoints.models import Device +from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector +from authentik.events.models import NotificationWebhookMapping +from authentik.lib.generators import generate_id +from authentik.lib.tests.utils import load_fixture + +TEST_HOST_UBUNTU = loads(load_fixture("fixtures/host_ubuntu.json")) +TEST_HOST_FEDORA = loads(load_fixture("fixtures/host_fedora.json")) +TEST_HOST_MACOS = loads(load_fixture("fixtures/host_macos.json")) +TEST_HOST_WINDOWS = loads(load_fixture("fixtures/host_windows.json")) + +TEST_HOST = {"hosts": [TEST_HOST_UBUNTU, TEST_HOST_MACOS, TEST_HOST_WINDOWS, TEST_HOST_FEDORA]} + + +class TestFleetConnector(APITestCase): + def setUp(self): + self.connector = FleetConnector.objects.create( + name=generate_id(), url="http://localhost", token=generate_id() + ) + + def test_sync(self): + controller = self.connector.controller(self.connector) + with Mocker() as mock: + mock.get( + "http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=0&per_page=50&device_mapping=true&populate_software=true&populate_users=true", + json=TEST_HOST, + ) + mock.get( + "http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=1&per_page=50&device_mapping=true&populate_software=true&populate_users=true", + json={"hosts": []}, + ) + controller.sync_endpoints() + device = Device.objects.filter( + identifier="VMware-56 4d 4a 5a b0 22 7b d7-9b a5 0b dc 8f f2 3b 60" + ).first() + self.assertIsNotNone(device) + self.assertEqual( + device.cached_facts.data, + { + "os": { + "arch": "x86_64", + "name": "Ubuntu", + "family": "linux", + "version": "24.04.3 LTS", + }, + "disks": [], + "vendor": {"fleetdm.com": {"policies": [], "agent_version": ""}}, + "network": {"hostname": "ubuntu-desktop", "interfaces": []}, + "hardware": { + "model": "VMware20,1", + "serial": "VMware-56 4d 4a 5a b0 22 7b d7-9b a5 0b dc 8f f2 3b 60", + "cpu_count": 2, + "cpu_name": "Intel(R) Core(TM) i5-10500T CPU @ 2.30GHz", + "manufacturer": "VMware, Inc.", + "memory_bytes": 2062721024, + }, + "software": [], + }, + ) + + def test_sync_headers(self): + mapping = NotificationWebhookMapping.objects.create( + name=generate_id(), expression="""return {"foo": "bar"}""" + ) + self.connector.headers_mapping = mapping + self.connector.save() + controller = self.connector.controller(self.connector) + with Mocker() as mock: + mock.get( + "http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=0&per_page=50&device_mapping=true&populate_software=true&populate_users=true", + json=TEST_HOST, + ) + mock.get( + "http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=1&per_page=50&device_mapping=true&populate_software=true&populate_users=true", + json={"hosts": []}, + ) + controller.sync_endpoints() + self.assertEqual(mock.call_count, 2) + self.assertEqual(mock.request_history[0].method, "GET") + self.assertEqual(mock.request_history[0].headers["foo"], "bar") + self.assertEqual(mock.request_history[1].method, "GET") + self.assertEqual(mock.request_history[1].headers["foo"], "bar") + + def test_map_host_linux(self): + controller = self.connector.controller(self.connector) + self.assertEqual( + controller.map_os(TEST_HOST_UBUNTU), + { + "arch": "x86_64", + "family": OSFamily.linux, + "name": "Ubuntu", + "version": "24.04.3 LTS", + }, + ) + self.assertEqual( + controller.map_os(TEST_HOST_FEDORA), + { + "arch": "x86_64", + "family": OSFamily.linux, + "name": "Fedora Linux", + "version": "43.0.0", + }, + ) + + def test_map_host_windows(self): + controller = self.connector.controller(self.connector) + self.assertEqual( + controller.map_os(TEST_HOST_WINDOWS), + { + "arch": "x86_64", + "family": OSFamily.windows, + "name": "Windows Server 2022 Datacenter 21H2", + "version": "10.0.20348.4405", + }, + ) + + def test_map_host_macos(self): + controller = self.connector.controller(self.connector) + self.assertEqual( + controller.map_os(TEST_HOST_MACOS), + { + "arch": "arm64e", + "family": OSFamily.macOS, + "name": "macOS", + "version": "26.0.1", + }, + ) diff --git a/authentik/enterprise/endpoints/connectors/fleet/urls.py b/authentik/enterprise/endpoints/connectors/fleet/urls.py new file mode 100644 index 0000000000..212a5bf79d --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/urls.py @@ -0,0 +1,3 @@ +from authentik.enterprise.endpoints.connectors.fleet.api import FleetConnectorViewSet + +api_urlpatterns = [("endpoints/fleet/connectors", FleetConnectorViewSet)] diff --git a/authentik/enterprise/settings.py b/authentik/enterprise/settings.py index 5014743ab4..e8a4164de1 100644 --- a/authentik/enterprise/settings.py +++ b/authentik/enterprise/settings.py @@ -3,6 +3,7 @@ TENANT_APPS = [ "authentik.enterprise.audit", "authentik.enterprise.endpoints.connectors.agent", + "authentik.enterprise.endpoints.connectors.fleet", "authentik.enterprise.policies.unique_password", "authentik.enterprise.providers.google_workspace", "authentik.enterprise.providers.microsoft_entra", diff --git a/blueprints/schema.json b/blueprints/schema.json index 4b2cbf9418..73acb24bf5 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -656,6 +656,46 @@ } } }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_endpoints_connectors_fleet.fleetconnector" + }, + "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_endpoints_connectors_fleet.fleetconnector_permissions" + }, + "attrs": { + "$ref": "#/$defs/model_authentik_endpoints_connectors_fleet.fleetconnector" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_endpoints_connectors_fleet.fleetconnector" + } + } + }, { "type": "object", "required": [ @@ -5432,6 +5472,10 @@ "authentik_endpoints_connectors_agent.view_devicetoken", "authentik_endpoints_connectors_agent.view_enrollment_token_key", "authentik_endpoints_connectors_agent.view_enrollmenttoken", + "authentik_endpoints_connectors_fleet.add_fleetconnector", + "authentik_endpoints_connectors_fleet.change_fleetconnector", + "authentik_endpoints_connectors_fleet.delete_fleetconnector", + "authentik_endpoints_connectors_fleet.view_fleetconnector", "authentik_enterprise.add_license", "authentik_enterprise.add_licenseusage", "authentik_enterprise.change_license", @@ -6319,6 +6363,11 @@ "type": "string", "minLength": 1, "title": "Name" + }, + "attributes": { + "type": "object", + "additionalProperties": true, + "title": "Attributes" } }, "required": [] @@ -6477,6 +6526,78 @@ } } }, + "model_authentik_endpoints_connectors_fleet.fleetconnector": { + "type": "object", + "properties": { + "connector_uuid": { + "type": "string", + "format": "uuid", + "title": "Connector uuid" + }, + "name": { + "type": "string", + "minLength": 1, + "title": "Name" + }, + "enabled": { + "type": "boolean", + "title": "Enabled" + }, + "url": { + "type": "string", + "format": "uri", + "maxLength": 200, + "minLength": 1, + "title": "Url" + }, + "token": { + "type": "string", + "minLength": 1, + "title": "Token" + }, + "headers_mapping": { + "type": "string", + "format": "uuid", + "title": "Headers mapping", + "description": "Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs" + }, + "map_users": { + "type": "boolean", + "title": "Map users" + }, + "map_teams_access_group": { + "type": "boolean", + "title": "Map teams access group" + } + }, + "required": [] + }, + "model_authentik_endpoints_connectors_fleet.fleetconnector_permissions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "permission" + ], + "properties": { + "permission": { + "type": "string", + "enum": [ + "add_fleetconnector", + "change_fleetconnector", + "delete_fleetconnector", + "view_fleetconnector" + ] + }, + "user": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, "model_authentik_enterprise.license": { "type": "object", "properties": { @@ -8161,6 +8282,7 @@ "authentik.blueprints", "authentik.enterprise.audit", "authentik.enterprise.endpoints.connectors.agent", + "authentik.enterprise.endpoints.connectors.fleet", "authentik.enterprise.policies.unique_password", "authentik.enterprise.providers.google_workspace", "authentik.enterprise.providers.microsoft_entra", @@ -8288,6 +8410,7 @@ "authentik_tasks_schedules.schedule", "authentik_brands.brand", "authentik_blueprints.blueprintinstance", + "authentik_endpoints_connectors_fleet.fleetconnector", "authentik_policies_unique_password.uniquepasswordpolicy", "authentik_providers_google_workspace.googleworkspaceprovider", "authentik_providers_google_workspace.googleworkspaceprovidermapping", @@ -10551,6 +10674,10 @@ "authentik_endpoints_connectors_agent.view_devicetoken", "authentik_endpoints_connectors_agent.view_enrollment_token_key", "authentik_endpoints_connectors_agent.view_enrollmenttoken", + "authentik_endpoints_connectors_fleet.add_fleetconnector", + "authentik_endpoints_connectors_fleet.change_fleetconnector", + "authentik_endpoints_connectors_fleet.delete_fleetconnector", + "authentik_endpoints_connectors_fleet.view_fleetconnector", "authentik_enterprise.add_license", "authentik_enterprise.add_licenseusage", "authentik_enterprise.change_license", diff --git a/schema.yml b/schema.yml index f3a6541071..50b8a6bd00 100644 --- a/schema.yml +++ b/schema.yml @@ -6447,6 +6447,196 @@ paths: $ref: '#/components/responses/ValidationErrorResponse' '403': $ref: '#/components/responses/GenericErrorResponse' + /endpoints/fleet/connectors/: + get: + operationId: endpoints_fleet_connectors_list + description: FleetConnector Viewset + parameters: + - $ref: '#/components/parameters/QueryName' + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QueryPaginationPage' + - $ref: '#/components/parameters/QueryPaginationPageSize' + - $ref: '#/components/parameters/QuerySearch' + tags: + - endpoints + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedFleetConnectorList' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + post: + operationId: endpoints_fleet_connectors_create + description: FleetConnector Viewset + tags: + - endpoints + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FleetConnectorRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/FleetConnector' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + /endpoints/fleet/connectors/{connector_uuid}/: + get: + operationId: endpoints_fleet_connectors_retrieve + description: FleetConnector Viewset + parameters: + - in: path + name: connector_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Fleet Connector. + required: true + tags: + - endpoints + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/FleetConnector' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + put: + operationId: endpoints_fleet_connectors_update + description: FleetConnector Viewset + parameters: + - in: path + name: connector_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Fleet Connector. + required: true + tags: + - endpoints + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FleetConnectorRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/FleetConnector' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + patch: + operationId: endpoints_fleet_connectors_partial_update + description: FleetConnector Viewset + parameters: + - in: path + name: connector_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Fleet Connector. + required: true + tags: + - endpoints + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedFleetConnectorRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/FleetConnector' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + delete: + operationId: endpoints_fleet_connectors_destroy + description: FleetConnector Viewset + parameters: + - in: path + name: connector_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Fleet Connector. + required: true + tags: + - endpoints + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + /endpoints/fleet/connectors/{connector_uuid}/used_by/: + get: + operationId: endpoints_fleet_connectors_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: connector_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Fleet Connector. + required: true + tags: + - endpoints + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' /enterprise/license/: get: operationId: enterprise_license_list @@ -19897,6 +20087,7 @@ paths: - authentik_endpoints_connectors_agent.agentconnector - authentik_endpoints_connectors_agent.agentdeviceuserbinding - authentik_endpoints_connectors_agent.enrollmenttoken + - authentik_endpoints_connectors_fleet.fleetconnector - authentik_enterprise.license - authentik_events.event - authentik_events.notification @@ -33121,6 +33312,7 @@ components: - authentik.blueprints - authentik.enterprise.audit - authentik.enterprise.endpoints.connectors.agent + - authentik.enterprise.endpoints.connectors.fleet - authentik.enterprise.policies.unique_password - authentik.enterprise.providers.google_workspace - authentik.enterprise.providers.microsoft_entra @@ -36019,6 +36211,9 @@ components: readOnly: true name: type: string + attributes: + type: object + additionalProperties: {} required: - name - pbm_uuid @@ -36028,6 +36223,9 @@ components: name: type: string minLength: 1 + attributes: + type: object + additionalProperties: {} required: - name DeviceChallenge: @@ -37801,6 +37999,89 @@ components: default: media required: - file + FleetConnector: + type: object + description: FleetConnector Serializer + properties: + connector_uuid: + type: string + format: uuid + name: + type: string + enabled: + type: boolean + component: + type: string + description: Get object component so that we know how to edit the object + readOnly: true + verbose_name: + type: string + description: Return object's verbose_name + readOnly: true + verbose_name_plural: + type: string + description: Return object's plural verbose_name + readOnly: true + meta_model_name: + type: string + description: Return internal model name + readOnly: true + url: + type: string + format: uri + maxLength: 200 + headers_mapping: + type: string + format: uuid + nullable: true + description: Configure additional headers to be sent. Mapping should return + a dictionary of key-value pairs + map_users: + type: boolean + map_teams_access_group: + type: boolean + required: + - component + - meta_model_name + - name + - url + - verbose_name + - verbose_name_plural + FleetConnectorRequest: + type: object + description: FleetConnector Serializer + properties: + connector_uuid: + type: string + format: uuid + name: + type: string + minLength: 1 + enabled: + type: boolean + url: + type: string + format: uri + minLength: 1 + maxLength: 200 + token: + type: string + writeOnly: true + minLength: 1 + headers_mapping: + type: string + format: uuid + nullable: true + description: Configure additional headers to be sent. Mapping should return + a dictionary of key-value pairs + map_users: + type: boolean + map_teams_access_group: + type: boolean + required: + - name + - token + - url Flow: type: object description: Flow Serializer @@ -41546,6 +41827,7 @@ components: - authentik_tasks_schedules.schedule - authentik_brands.brand - authentik_blueprints.blueprintinstance + - authentik_endpoints_connectors_fleet.fleetconnector - authentik_policies_unique_password.uniquepasswordpolicy - authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovidermapping @@ -42743,13 +43025,22 @@ components: - userinfo_endpoint OperatingSystem: type: object + description: |- + For example: + {"family":"linux","name":"Ubuntu","version":"24.04.3 LTS (Noble Numbat)","arch":"amd64"} + {"family": "windows","name":"Server 2022 Datacenter","version":"10.0.20348.4405","arch":"amd64"} + {"family": "windows","name":"Server 2022 Datacenter","version":"10.0.20348.4405","arch":"amd64"} + {"family": "mac_os", "name": "", "version": "26.2", "arch": "arm64"} properties: family: $ref: '#/components/schemas/DeviceFactsOSFamily' name: type: string + description: Operating System name, such as 'Server 2022' or 'Ubuntu' version: type: string + description: Operating System version, must always be the version number + but may contain build name arch: type: string required: @@ -42757,15 +43048,24 @@ components: - family OperatingSystemRequest: type: object + description: |- + For example: + {"family":"linux","name":"Ubuntu","version":"24.04.3 LTS (Noble Numbat)","arch":"amd64"} + {"family": "windows","name":"Server 2022 Datacenter","version":"10.0.20348.4405","arch":"amd64"} + {"family": "windows","name":"Server 2022 Datacenter","version":"10.0.20348.4405","arch":"amd64"} + {"family": "mac_os", "name": "", "version": "26.2", "arch": "arm64"} properties: family: $ref: '#/components/schemas/DeviceFactsOSFamily' name: type: string minLength: 1 + description: Operating System name, such as 'Server 2022' or 'Ubuntu' version: type: string minLength: 1 + description: Operating System version, must always be the version number + but may contain build name arch: type: string minLength: 1 @@ -43535,6 +43835,21 @@ components: required: - pagination - results + PaginatedFleetConnectorList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/FleetConnector' + autocomplete: + $ref: '#/components/schemas/Autocomplete' + required: + - pagination + - results + - autocomplete PaginatedFlowList: type: object properties: @@ -46154,6 +46469,9 @@ components: name: type: string minLength: 1 + attributes: + type: object + additionalProperties: {} PatchedDeviceUserBindingRequest: type: object description: PolicyBinding Serializer @@ -46498,6 +46816,37 @@ components: expression: type: string minLength: 1 + PatchedFleetConnectorRequest: + type: object + description: FleetConnector Serializer + properties: + connector_uuid: + type: string + format: uuid + name: + type: string + minLength: 1 + enabled: + type: boolean + url: + type: string + format: uri + minLength: 1 + maxLength: 200 + token: + type: string + writeOnly: true + minLength: 1 + headers_mapping: + type: string + format: uuid + nullable: true + description: Configure additional headers to be sent. Mapping should return + a dictionary of key-value pairs + map_users: + type: boolean + map_teams_access_group: + type: boolean PatchedFlowRequest: type: object description: Flow Serializer @@ -55950,6 +56299,7 @@ components: enum: - goauthentik.io/@merged - goauthentik.io/platform + - fleetdm.com type: string Version: type: object diff --git a/web/authentik/connectors/fleet.svg b/web/authentik/connectors/fleet.svg new file mode 100644 index 0000000000..2c8e236e56 --- /dev/null +++ b/web/authentik/connectors/fleet.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web/src/admin/endpoints/connectors/ConnectorViewPage.ts b/web/src/admin/endpoints/connectors/ConnectorViewPage.ts index c98b9eb360..aa9f8b420e 100644 --- a/web/src/admin/endpoints/connectors/ConnectorViewPage.ts +++ b/web/src/admin/endpoints/connectors/ConnectorViewPage.ts @@ -1,4 +1,5 @@ import "#admin/endpoints/connectors/agent/AgentConnectorViewPage"; +import "#admin/endpoints/connectors/fleet/FleetConnectorViewPage"; import "#elements/EmptyState"; import "#elements/buttons/SpinnerButton/ak-spinner-button"; @@ -41,6 +42,10 @@ export class ConnectorViewPage extends AKElement { return html``; + case "ak-endpoints-connector-fleet-form": + return html``; default: return html`

Invalid connector type ${this.connector?.component}

`; } diff --git a/web/src/admin/endpoints/connectors/ConnectorWizard.ts b/web/src/admin/endpoints/connectors/ConnectorWizard.ts index b9cc724f62..e6248c8d1c 100644 --- a/web/src/admin/endpoints/connectors/ConnectorWizard.ts +++ b/web/src/admin/endpoints/connectors/ConnectorWizard.ts @@ -1,5 +1,6 @@ import "#admin/common/ak-license-notice"; import "#admin/endpoints/connectors/agent/AgentConnectorForm"; +import "#admin/endpoints/connectors/fleet/FleetConnectorForm"; import "#elements/wizard/FormWizardPage"; import "#elements/wizard/TypeCreateWizardPage"; import "#elements/wizard/Wizard"; @@ -8,6 +9,7 @@ import { DEFAULT_CONFIG } from "#common/api/config"; import { AKElement } from "#elements/Base"; import { StrictUnsafe } from "#elements/utils/unsafe"; +import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage"; import { Wizard } from "#elements/wizard/Wizard"; import { EndpointsApi, TypeCreate } from "@goauthentik/api"; @@ -48,6 +50,7 @@ export class EndpointConnectorWizard extends AKElement { ) => { if (!this.wizard) return; const idx = this.wizard.steps.indexOf("initial") + 1; diff --git a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts index 916512fada..ac40c603df 100644 --- a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts +++ b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts @@ -1,5 +1,6 @@ import "#admin/endpoints/connectors/ConnectorWizard"; import "#admin/endpoints/connectors/agent/AgentConnectorForm"; +import "#admin/endpoints/connectors/fleet/FleetConnectorForm"; import "#elements/forms/DeleteBulkForm"; import "#elements/forms/ModalForm"; diff --git a/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts b/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts new file mode 100644 index 0000000000..804688f980 --- /dev/null +++ b/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts @@ -0,0 +1,95 @@ +import "#components/ak-secret-text-input"; +import "#components/ak-switch-input"; +import "#components/ak-text-input"; +import "#elements/forms/FormGroup"; + +import { DEFAULT_CONFIG } from "#common/api/config"; + +import { ModelForm } from "#elements/forms/ModelForm"; + +import { EndpointsApi, FleetConnector, FleetConnectorRequest } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { html } from "lit"; +import { customElement } from "lit/decorators.js"; + +@customElement("ak-endpoints-connector-fleet-form") +export class FleetConnectorForm extends ModelForm { + loadInstance(pk: string): Promise { + return new EndpointsApi(DEFAULT_CONFIG).endpointsFleetConnectorsRetrieve({ + connectorUuid: pk, + }); + } + + public override getSuccessMessage(): string { + return this.instance + ? msg("Successfully updated Fleet connector.") + : msg("Successfully created Fleet connector."); + } + + async send(data: FleetConnector): Promise { + if (this.instance) { + return new EndpointsApi(DEFAULT_CONFIG).endpointsFleetConnectorsPartialUpdate({ + connectorUuid: this.instance.connectorUuid!, + patchedFleetConnectorRequest: data, + }); + } + return new EndpointsApi(DEFAULT_CONFIG).endpointsFleetConnectorsCreate({ + fleetConnectorRequest: data as unknown as FleetConnectorRequest, + }); + } + + renderForm() { + return html` + + +
+ + + + + +
+
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-endpoints-connector-fleet-form": FleetConnectorForm; + } +} diff --git a/web/src/admin/endpoints/connectors/fleet/FleetConnectorViewPage.ts b/web/src/admin/endpoints/connectors/fleet/FleetConnectorViewPage.ts new file mode 100644 index 0000000000..f19c0b5a30 --- /dev/null +++ b/web/src/admin/endpoints/connectors/fleet/FleetConnectorViewPage.ts @@ -0,0 +1,157 @@ +import "#elements/Tabs"; +import "#components/events/ObjectChangelog"; +import "#admin/rbac/ObjectPermissionsPage"; +import "#elements/tasks/ScheduleList"; +import "#elements/tasks/TaskList"; + +import { DEFAULT_CONFIG } from "#common/api/config"; +import { APIError, parseAPIResponseError } from "#common/errors/network"; + +import { AKElement } from "#elements/Base"; + +import { setPageDetails } from "#components/ak-page-navbar"; + +import { + EndpointsApi, + FleetConnector, + ModelEnum, + RbacPermissionsAssignedByRolesListModelEnum, +} from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { CSSResult, html, nothing, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; +import PFPage from "@patternfly/patternfly/components/Page/page.css"; +import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; + +const [FLEET_CONNECTOR_APP_LABEL, FLEET_CONNECTOR_MODEL_NAME] = + ModelEnum.AuthentikEndpointsConnectorsFleetFleetconnector.split("."); + +@customElement("ak-endpoints-connector-fleet-view") +export class FleetConnectorViewPage extends AKElement { + @property({ type: String }) + public connectorId?: string; + + @state() + protected connector?: FleetConnector; + + @state() + protected error?: APIError; + + static styles: CSSResult[] = [PFCard, PFPage, PFGrid, PFButton, PFDescriptionList]; + + protected fetchDevice(id: string) { + new EndpointsApi(DEFAULT_CONFIG) + .endpointsFleetConnectorsRetrieve({ connectorUuid: id }) + .then((conn) => { + this.connector = conn; + }) + .catch(async (error) => { + this.error = await parseAPIResponseError(error); + }); + } + + public override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has("connectorId") && this.connectorId) { + this.fetchDevice(this.connectorId); + } + } + + public override updated(changed: PropertyValues) { + super.updated(changed); + setPageDetails({ + icon: "pf-icon pf-icon-data-source", + header: this.connector?.name, + description: this.connector?.verboseName, + }); + } + + protected renderTabOverview() { + return html`
+
+
+
+
${msg("Schedules")}
+
+
+ +
+
+
+
+
+
+
${msg("Tasks")}
+
+
+ +
+
+
+
`; + } + + render() { + if (!this.connector) { + return nothing; + } + return html` +
+ ${this.renderTabOverview()} +
+
+
+
+ + +
+
+
+ +
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-endpoints-connector-fleet-view": FleetConnectorViewPage; + } +} diff --git a/web/src/admin/endpoints/devices/DeviceViewPage.ts b/web/src/admin/endpoints/devices/DeviceViewPage.ts index 89794210de..78476bc993 100644 --- a/web/src/admin/endpoints/devices/DeviceViewPage.ts +++ b/web/src/admin/endpoints/devices/DeviceViewPage.ts @@ -2,6 +2,7 @@ import "#components/ak-status-label"; import "#admin/endpoints/devices/BoundDeviceUsersList"; import "#admin/endpoints/devices/facts/DeviceProcessTable"; import "#admin/endpoints/devices/facts/DeviceUserTable"; +import "#admin/endpoints/devices/facts/DeviceSoftwareTable"; import "#admin/endpoints/devices/facts/DeviceGroupTable"; import "#admin/endpoints/devices/DeviceForm"; import "#elements/forms/ModalForm"; @@ -192,9 +193,7 @@ export class DeviceViewPage extends AKElement { return [ html`${conn.connectorObj.name}`, html`
- ${msg( - str`Agent version: ${this.agentVersion(conn) ?? "-"}`, - )} + ${this.agentVersion(conn) ?? "-"}
${conn.latestSnapshot?.created @@ -226,7 +225,7 @@ export class DeviceViewPage extends AKElement { const vendorData = vendorContainer[conn.latestSnapshot.vendor]; if (!vendorData) return; if (!("agent_version" in vendorData)) return; - return vendorData.agent_version; + return msg(str`Agent version: ${vendorData.agent_version ?? "-"}`); } renderProcesses() { @@ -256,6 +255,15 @@ export class DeviceViewPage extends AKElement { >`; } + renderSoftware() { + if (!this.device) { + return nothing; + } + return html``; + } + render() { return html`
@@ -299,6 +307,16 @@ export class DeviceViewPage extends AKElement { > ${this.renderGroups()}
+
+ ${this.renderSoftware()} +
`; } diff --git a/web/src/admin/endpoints/devices/facts/DeviceSoftwareTable.ts b/web/src/admin/endpoints/devices/facts/DeviceSoftwareTable.ts new file mode 100644 index 0000000000..b799c91125 --- /dev/null +++ b/web/src/admin/endpoints/devices/facts/DeviceSoftwareTable.ts @@ -0,0 +1,59 @@ +import { groupBy, GroupResult } from "#common/utils"; + +import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table"; +import { SlottedTemplateResult } from "#elements/types"; + +import { EndpointDeviceDetails, Software } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("ak-endpoints-device-software-table") +export class DeviceSoftwareTable extends Table { + @property({ attribute: false }) + device?: EndpointDeviceDetails; + + protected async apiEndpoint(): Promise> { + const items = (this.device?.facts.data.software || []).sort((a, b) => + a.name.localeCompare(b.name), + ); + return { + pagination: { + count: items.length, + current: 1, + totalPages: 1, + startIndex: 1, + endIndex: items.length, + next: 0, + previous: 0, + }, + results: items, + }; + } + + protected groupBy(items: Software[]): GroupResult[] { + return groupBy(items, (item) => item.source); + } + + protected columns: TableColumn[] = [ + [msg("Name")], + [msg("Version")], + [msg("Source")], + [msg("Path")], + ]; + protected row(item: Software): SlottedTemplateResult[] { + return [ + html`${item.name}`, + html`${item.version ?? "-"}`, + html`${item.source}`, + html`${item.path ?? "-"}`, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-endpoints-device-software-table": DeviceSoftwareTable; + } +} diff --git a/web/src/elements/wizard/TypeCreateWizardPage.ts b/web/src/elements/wizard/TypeCreateWizardPage.ts index 69075344a9..3148814856 100644 --- a/web/src/elements/wizard/TypeCreateWizardPage.ts +++ b/web/src/elements/wizard/TypeCreateWizardPage.ts @@ -94,61 +94,66 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { }; protected renderGrid(): TemplateResult { - return html`
- ${this.types.map((type, idx) => { - const disabled = !!(type.requiresEnterprise && !this.hasEnterpriseLicense); + return html`${this.hasSlotted("above-form") + ? html`
` + : nothing} +
+ ${this.types.map((type, idx) => { + const disabled = !!(type.requiresEnterprise && !this.hasEnterpriseLicense); - const selected = this.selectedType === type; + const selected = this.selectedType === type; - return html`
{ - if (disabled) return; + return html`
{ + if (disabled) return; - this.#selectDispatch(type); - this.selectedType = type; - }} - > - ${type.iconUrl - ? html`` - : nothing} -
${type.name}
-
${type.description}
- ${disabled - ? html` ` - : nothing} -
`; - })} -
`; + this.#selectDispatch(type); + this.selectedType = type; + }} + > + ${type.iconUrl + ? html`` + : nothing} +
+ ${type.name} +
+
${type.description}
+ ${disabled + ? html` ` + : nothing} +
`; + })} +
`; } renderList(): TemplateResult {