diff --git a/authentik/endpoints/api/device_access_group.py b/authentik/endpoints/api/device_access_group.py index 72dc209c376b..29d64d1fd622 100644 --- a/authentik/endpoints/api/device_access_group.py +++ b/authentik/endpoints/api/device_access_group.py @@ -12,6 +12,7 @@ class Meta: fields = [ "pbm_uuid", "name", + "attributes", ] diff --git a/authentik/endpoints/migrations/0004_deviceaccessgroup_attributes.py b/authentik/endpoints/migrations/0004_deviceaccessgroup_attributes.py new file mode 100644 index 000000000000..1d705ab1a0af --- /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 52a908df7245..303d79e7ea05 100644 --- a/authentik/endpoints/models.py +++ b/authentik/endpoints/models.py @@ -175,7 +175,7 @@ def schedule_specs(self) -> list[ScheduleSpec]: ] -class DeviceAccessGroup(PolicyBindingModel): +class DeviceAccessGroup(AttributesMixin, 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 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/endpoints/connectors/fleet/api.py b/authentik/enterprise/endpoints/connectors/fleet/api.py new file mode 100644 index 000000000000..90d11baa7a7a --- /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 000000000000..f14f9cab7836 --- /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 000000000000..609017340a46 --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/controller.py @@ -0,0 +1,189 @@ +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, + ) + ) + ) + + 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 = 1 + while True: + 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: + break + 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"] == "debian": + 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"] == "ipados" or host["platform"] == "ios": + return OSFamily.iOS + return OSFamily.other + + 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": delete_none_values( + { + "arch": self.or_none(host["cpu_type"]), + "family": FleetController.os_family(host), + "name": self.or_none(host["platform_like"]), + "version": self.or_none(host["os_version"]), + } + ), + "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 000000000000..68a0c179162b --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.9 on 2025-12-08 23:54 + +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", "0013_delete_systemtask"), + ] + + 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 000000000000..e69de29bb2d1 diff --git a/authentik/enterprise/endpoints/connectors/fleet/models.py b/authentik/enterprise/endpoints/connectors/fleet/models.py new file mode 100644 index 000000000000..f958ac7d766f --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/models.py @@ -0,0 +1,51 @@ +from typing import TYPE_CHECKING + +from django.db import models +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 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.py b/authentik/enterprise/endpoints/connectors/fleet/tests.py new file mode 100644 index 000000000000..d23f3258f88f --- /dev/null +++ b/authentik/enterprise/endpoints/connectors/fleet/tests.py @@ -0,0 +1,147 @@ +from requests_mock import Mocker +from rest_framework.test import APITestCase + +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 + +TEST_HOST = { + "hosts": [ + { + "created_at": "2025-09-20T17:46:17Z", + "updated_at": "2025-12-05T17:13:24Z", + "software": None, + "software_updated_at": "2025-12-05T17:13:24Z", + "id": 8, + "detail_updated_at": "2025-12-05T17:13:25Z", + "label_updated_at": "2025-12-05T17:13:25Z", + "policy_updated_at": "2025-12-05T16:41:15Z", + "last_enrolled_at": "2025-09-20T17:46:17Z", + "seen_time": "2025-12-05T17:38:07Z", + "refetch_requested": False, + "hostname": "oci-kube-1", + "uuid": "72a6f06a-d27d-45ec-996d-0ca298f50c61", + "platform": "ubuntu", + "osquery_version": "5.20.0", + "orbit_version": None, + "fleet_desktop_version": None, + "scripts_enabled": None, + "os_version": "Ubuntu 24.04.3 LTS", + "build": "", + "platform_like": "debian", + "code_name": "noble", + "uptime": 8454318000000000, + "memory": 25141047296, + "cpu_type": "aarch64", + "cpu_subtype": "0", + "cpu_brand": "", + "cpu_physical_cores": 4, + "cpu_logical_cores": 4, + "hardware_vendor": "QEMU", + "hardware_model": "KVM Virtual Machine", + "hardware_version": "virt-4.2", + "hardware_serial": "8A19B472-3FD7-475F-88F3-B2C2D0BC0D1A", + "computer_name": "oci-kube-1", + "public_ip": "130.61.116.187", + "primary_ip": "10.120.90.154", + "primary_mac": "02:00:17:01:d0:d2", + "distributed_interval": 10, + "config_tls_refresh": 60, + "logger_tls_period": 10, + "team_id": 2, + "pack_stats": None, + "team_name": "prod", + "gigs_disk_space_available": 74.88, + "percent_disk_space_available": 72, + "gigs_total_disk_space": 103.86, + "gigs_all_disk_space": 103.86, + "issues": { + "failing_policies_count": 0, + "critical_vulnerabilities_count": 0, + "total_issues_count": 0, + }, + "device_mapping": None, + "mdm": { + "enrollment_status": None, + "dep_profile_error": False, + "server_url": None, + "name": "", + "encryption_key_available": False, + "connected_to_fleet": False, + }, + "refetch_critical_queries_until": None, + "last_restarted_at": "2025-08-29T20:48:07Z", + "status": "online", + "display_text": "oci-kube-1", + "display_name": "oci-kube-1", + } + ] +} + + +class TestFleetConnector(APITestCase): + + def test_sync(self): + connector = FleetConnector.objects.create( + name=generate_id(), url="http://localhost", token=generate_id() + ) + controller = connector.controller(connector) + with Mocker() as mock: + 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=TEST_HOST, + ) + mock.get( + "http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=2&per_page=50&device_mapping=true&populate_software=true&populate_users=true", + json={"hosts": []}, + ) + controller.sync_endpoints() + device = Device.objects.filter(identifier="8A19B472-3FD7-475F-88F3-B2C2D0BC0D1A").first() + self.assertIsNotNone(device) + self.assertEqual( + device.cached_facts.data, + { + "os": { + "arch": "aarch64", + "name": "debian", + "family": "linux", + "version": "Ubuntu 24.04.3 LTS", + }, + "disks": [], + "vendor": {"fleetdm.com": {"policies": [], "agent_version": ""}}, + "network": {"hostname": "oci-kube-1", "interfaces": []}, + "hardware": { + "model": "KVM Virtual Machine", + "serial": "8A19B472-3FD7-475F-88F3-B2C2D0BC0D1A", + "cpu_count": 4, + "manufacturer": "QEMU", + "memory_bytes": 25141047296, + }, + "software": [], + }, + ) + + def test_sync_headers(self): + mapping = NotificationWebhookMapping.objects.create( + name=generate_id(), expression="""return {"foo": "bar"}""" + ) + connector = FleetConnector.objects.create( + name=generate_id(), url="http://localhost", token=generate_id(), headers_mapping=mapping + ) + controller = connector.controller(connector) + with Mocker() as mock: + 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=TEST_HOST, + ) + mock.get( + "http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=2&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") diff --git a/authentik/enterprise/endpoints/connectors/fleet/urls.py b/authentik/enterprise/endpoints/connectors/fleet/urls.py new file mode 100644 index 000000000000..212a5bf79ddf --- /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 720d7dd00045..e7455b2da9eb 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 efd00c40595e..612475720739 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -616,6 +616,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": [ @@ -5350,6 +5390,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", @@ -6427,6 +6471,77 @@ } } }, + "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": "integer", + "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": { @@ -8293,6 +8408,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", @@ -8418,6 +8534,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", @@ -10678,6 +10795,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 254f3a3b65fe..b4e06172156b 100644 --- a/schema.yml +++ b/schema.yml @@ -6237,6 +6237,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 @@ -19577,6 +19767,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 @@ -32681,6 +32872,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 @@ -35529,6 +35721,9 @@ components: readOnly: true name: type: string + attributes: + type: object + additionalProperties: {} required: - name - pbm_uuid @@ -35538,6 +35733,9 @@ components: name: type: string minLength: 1 + attributes: + type: object + additionalProperties: {} required: - name DeviceChallenge: @@ -37311,6 +37509,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 @@ -41048,6 +41329,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 @@ -43009,6 +43291,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: @@ -45674,6 +45971,9 @@ components: name: type: string minLength: 1 + attributes: + type: object + additionalProperties: {} PatchedDeviceUserBindingRequest: type: object description: PolicyBinding Serializer @@ -46030,6 +46330,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 diff --git a/web/src/admin/endpoints/connectors/ConnectorViewPage.ts b/web/src/admin/endpoints/connectors/ConnectorViewPage.ts index c98b9eb36046..aa9f8b420e1e 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 2f056f845d0b..2ee2fa1db62b 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/forms/ProxyForm"; import "#elements/wizard/FormWizardPage"; import "#elements/wizard/TypeCreateWizardPage"; diff --git a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts index 52b81580d12b..359a8a89ca91 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/ProxyForm"; 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 000000000000..804688f9803d --- /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 000000000000..f19c0b5a300a --- /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; + } +}