endpoints: FleetDM connector (#18589)

* enterprise/endpoints/connectors/fleet: init

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	blueprints/schema.json
#	schema.yml

* add ui

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix desc

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add configurable headers

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* 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 <jens@goauthentik.io>

* add attributes to device access group

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add option to map device team

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* cleanup

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update schema

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* switch connector to grid, add icons

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix pagination

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add software tab

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix pages in test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more test devices

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add fedora test machine

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* better formatting for OS version

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: GirlBossRush <592134+GirlBossRush@users.noreply.github.com>
This commit is contained in:
Jens L.
2026-01-23 21:40:28 +01:00
committed by GitHub
parent 0a10b81d1d
commit e2cb1a8d0c
31 changed files with 1685 additions and 60 deletions
@@ -12,6 +12,7 @@ class DeviceAccessGroupSerializer(ModelSerializer):
fields = [
"pbm_uuid",
"name",
"attributes",
]
@@ -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 (
+17 -3
View File
@@ -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)
@@ -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),
),
]
+1 -1
View File
@@ -175,7 +175,7 @@ class Connector(ScheduledModel, SerializerModel):
]
class DeviceAccessGroup(SerializerModel, PolicyBindingModel):
class DeviceAccessGroup(AttributesMixin, SerializerModel, PolicyBindingModel):
name = models.TextField(unique=True)
@@ -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"]
@@ -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
@@ -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
@@ -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",),
),
]
@@ -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")
@@ -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
}
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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",
},
)
@@ -0,0 +1,3 @@
from authentik.enterprise.endpoints.connectors.fleet.api import FleetConnectorViewSet
api_urlpatterns = [("endpoints/fleet/connectors", FleetConnectorViewSet)]
+1
View File
@@ -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",
+127
View File
@@ -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",
+350
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.97739 6C4.62176 6 5.95479 4.65685 5.95479 3C5.95479 1.34314 4.62176 0 2.97739 0C1.33302 0 0 1.34314 0 3C0 4.65685 1.33302 6 2.97739 6Z" fill="#63C740"/>
<path d="M11.9097 6C13.554 6 14.8871 4.65685 14.8871 3C14.8871 1.34314 13.554 0 11.9097 0C10.2653 0 8.93225 1.34314 8.93225 3C8.93225 4.65685 10.2653 6 11.9097 6Z" fill="#5CABDF"/>
<path d="M20.8418 6C22.4861 6 23.8192 4.65685 23.8192 3C23.8192 1.34314 22.4861 0 20.8418 0C19.1974 0 17.8644 1.34314 17.8644 3C17.8644 4.65685 19.1974 6 20.8418 6Z" fill="#D66C7B"/>
<path d="M2.97739 15C4.62176 15 5.95479 13.6569 5.95479 12C5.95479 10.3432 4.62176 9.00002 2.97739 9.00002C1.33302 9.00002 0 10.3432 0 12C0 13.6569 1.33302 15 2.97739 15Z" fill="#C98DEF"/>
<path d="M11.9097 15C13.554 15 14.8871 13.6569 14.8871 12C14.8871 10.3432 13.554 9.00002 11.9097 9.00002C10.2653 9.00002 8.93225 10.3432 8.93225 12C8.93225 13.6569 10.2653 15 11.9097 15Z" fill="#FAA669"/>
<path d="M2.97739 24C4.62176 24 5.95479 22.6569 5.95479 21C5.95479 19.3432 4.62176 18 2.97739 18C1.33302 18 0 19.3432 0 21C0 22.6569 1.33302 24 2.97739 24Z" fill="#3AEFC4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -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`<ak-endpoints-connector-agent-view
connectorID=${ifDefined(this.connector.connectorUuid)}
></ak-endpoints-connector-agent-view>`;
case "ak-endpoints-connector-fleet-form":
return html`<ak-endpoints-connector-fleet-view
connectorID=${ifDefined(this.connector.connectorUuid)}
></ak-endpoints-connector-fleet-view>`;
default:
return html`<p>Invalid connector type ${this.connector?.component}</p>`;
}
@@ -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 {
<ak-wizard-page-type-create
slot="initial"
.types=${this.connectorTypes}
layout=${TypeCreateWizardPageLayouts.grid}
@select=${(ev: CustomEvent<TypeCreate>) => {
if (!this.wizard) return;
const idx = this.wizard.steps.indexOf("initial") + 1;
@@ -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";
@@ -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<FleetConnector, string> {
loadInstance(pk: string): Promise<FleetConnector> {
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<FleetConnector> {
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`<ak-text-input
name="name"
placeholder=${msg("Connector name...")}
label=${msg("Connector name")}
value=${this.instance?.name ?? ""}
required
></ak-text-input>
<ak-switch-input
name="enabled"
label=${msg("Enabled")}
?checked=${this.instance?.enabled ?? true}
></ak-switch-input>
<ak-form-group label=${msg("Fleet settings")} open>
<div class="pf-c-form">
<ak-text-input
name="url"
label=${msg("Fleet Server URL")}
value=${this.instance?.url ?? ""}
required
input-hint="code"
>
</ak-text-input>
<ak-secret-text-input
label=${msg("Fleet API Token")}
name="token"
?revealed=${!this.instance}
></ak-secret-text-input>
<ak-switch-input
name="mapUsers"
label=${msg("Map users")}
?checked=${this.instance?.mapUsers ?? true}
help=${msg(
"When enabled, users detected by Fleet will be mapped in authentik, granting them access to the device.",
)}
></ak-switch-input>
<ak-switch-input
name="mapTeamsAccessGroup"
label=${msg("Map teams to device access group")}
?checked=${this.instance?.mapTeamsAccessGroup ?? false}
help=${msg(
"When enabled, Fleet teams will be mapped to Device access groups. Missing device access groups are automatically created. Devices assigned to a different group are not re-assigned",
)}
></ak-switch-input>
</div>
</ak-form-group>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-endpoints-connector-fleet-form": FleetConnectorForm;
}
}
@@ -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<this>) {
if (changedProperties.has("connectorId") && this.connectorId) {
this.fetchDevice(this.connectorId);
}
}
public override updated(changed: PropertyValues<this>) {
super.updated(changed);
setPageDetails({
icon: "pf-icon pf-icon-data-source",
header: this.connector?.name,
description: this.connector?.verboseName,
});
}
protected renderTabOverview() {
return html`<div
class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"
>
<div class="pf-l-grid__item pf-m-12-col pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__title">${msg("Schedules")}</div>
</div>
<div class="pf-c-card__body">
<ak-schedule-list
.relObjAppLabel=${FLEET_CONNECTOR_APP_LABEL}
.relObjModel=${FLEET_CONNECTOR_MODEL_NAME}
.relObjId="${this.connector?.connectorUuid}"
></ak-schedule-list>
</div>
</div>
</div>
<div class="pf-l-grid__item pf-m-12-col pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__title">${msg("Tasks")}</div>
</div>
<div class="pf-c-card__body">
<ak-task-list
.relObjAppLabel=${FLEET_CONNECTOR_APP_LABEL}
.relObjModel=${FLEET_CONNECTOR_MODEL_NAME}
.relObjId="${this.connector?.connectorUuid}"
></ak-task-list>
</div>
</div>
</div>
</div> `;
}
render() {
if (!this.connector) {
return nothing;
}
return html`<ak-tabs>
<div
role="tabpanel"
tabindex="0"
slot="page-overview"
id="page-overview"
aria-label="${msg("Overview")}"
>
${this.renderTabOverview()}
</div>
<div
role="tabpanel"
tabindex="0"
slot="page-changelog"
id="page-changelog"
aria-label="${msg("Changelog")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-object-changelog
targetModelPk=${this.connector?.connectorUuid || ""}
targetModelName=${this.connector?.metaModelName || ""}
>
</ak-object-changelog>
</div>
</div>
</div>
<ak-rbac-object-permission-page
role="tabpanel"
tabindex="0"
slot="page-permissions"
id="page-permissions"
aria-label=${msg("Permissions")}
model=${RbacPermissionsAssignedByRolesListModelEnum.AuthentikEndpointsConnectorsFleetFleetconnector}
objectPk=${this.connector.connectorUuid!}
></ak-rbac-object-permission-page>
</ak-tabs> `;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-endpoints-connector-fleet-view": FleetConnectorViewPage;
}
}
@@ -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`<div class="pf-c-description-list__text">
${msg(
str`Agent version: ${this.agentVersion(conn) ?? "-"}`,
)}
${this.agentVersion(conn) ?? "-"}
</div>
<div class="pf-c-description-list__text">
${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 {
></ak-endpoints-device-groups-table>`;
}
renderSoftware() {
if (!this.device) {
return nothing;
}
return html`<ak-endpoints-device-software-table
.device=${this.device}
></ak-endpoints-device-software-table>`;
}
render() {
return html`<main part="main">
<ak-tabs part="tabs">
@@ -299,6 +307,16 @@ export class DeviceViewPage extends AKElement {
>
${this.renderGroups()}
</div>
<div
role="tabpanel"
tabindex="0"
slot="page-software"
id="page-software"
aria-label="${msg("Software")}"
class="pf-c-page__main-section"
>
${this.renderSoftware()}
</div>
</ak-tabs>
</main>`;
}
@@ -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<Software> {
@property({ attribute: false })
device?: EndpointDeviceDetails;
protected async apiEndpoint(): Promise<PaginatedResponse<Software>> {
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<Software>[] {
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;
}
}
+57 -52
View File
@@ -94,61 +94,66 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
};
protected renderGrid(): TemplateResult {
return html`<div
role="listbox"
aria-label="${msg("Select a provider type")}"
class="pf-l-grid pf-m-gutter"
data-ouid-component-type="ak-type-create-grid"
>
${this.types.map((type, idx) => {
const disabled = !!(type.requiresEnterprise && !this.hasEnterpriseLicense);
return html`${this.hasSlotted("above-form")
? html`<div class="pf-c-page__main-section"><slot name="above-form"></slot></div>`
: nothing}
<div
role="listbox"
aria-label="${msg("Select a provider type")}"
class="pf-l-grid pf-m-gutter"
data-ouid-component-type="ak-type-create-grid"
>
${this.types.map((type, idx) => {
const disabled = !!(type.requiresEnterprise && !this.hasEnterpriseLicense);
const selected = this.selectedType === type;
const selected = this.selectedType === type;
return html`<div
class=${classMap({
"pf-l-grid__item": true,
"pf-m-3-col": true,
"pf-c-card": true,
"pf-m-non-selectable-raised": disabled,
"ak-m-enterprise-only": disabled,
"pf-m-selectable-raised": !disabled,
"pf-m-selected-raised": selected,
})}
tabindex=${idx}
role="option"
aria-disabled="${disabled ? "true" : "false"}"
aria-selected="${selected ? "true" : "false"}"
aria-label="${type.name}"
aria-describedby="${type.description}"
@click=${() => {
if (disabled) return;
return html`<div
class=${classMap({
"pf-l-grid__item": true,
"pf-m-3-col": true,
"pf-c-card": true,
"pf-m-non-selectable-raised": disabled,
"ak-m-enterprise-only": disabled,
"pf-m-selectable-raised": !disabled,
"pf-m-selected-raised": selected,
})}
tabindex=${idx}
role="option"
aria-disabled="${disabled ? "true" : "false"}"
aria-selected="${selected ? "true" : "false"}"
aria-label="${type.name}"
aria-describedby="${type.description}"
@click=${() => {
if (disabled) return;
this.#selectDispatch(type);
this.selectedType = type;
}}
>
${type.iconUrl
? html`<div role="presentation" class="pf-c-card__header">
<div role="presentation" class="pf-c-card__header-main">
<img
aria-hidden="true"
src=${type.iconUrl}
alt=${msg(str`${type.name} Icon`)}
/>
</div>
</div>`
: nothing}
<div role="heading" aria-level="2" class="pf-c-card__title">${type.name}</div>
<div role="presentational" class="pf-c-card__body">${type.description}</div>
${disabled
? html`<div class="pf-c-card__footer">
<ak-license-notice></ak-license-notice>
</div> `
: nothing}
</div>`;
})}
</div>`;
this.#selectDispatch(type);
this.selectedType = type;
}}
>
${type.iconUrl
? html`<div role="presentation" class="pf-c-card__header">
<div role="presentation" class="pf-c-card__header-main">
<img
aria-hidden="true"
src=${type.iconUrl}
alt=${msg(str`${type.name} Icon`)}
/>
</div>
</div>`
: nothing}
<div role="heading" aria-level="2" class="pf-c-card__title">
${type.name}
</div>
<div role="presentational" class="pf-c-card__body">${type.description}</div>
${disabled
? html`<div class="pf-c-card__footer">
<ak-license-notice></ak-license-notice>
</div> `
: nothing}
</div>`;
})}
</div>`;
}
renderList(): TemplateResult {