authentik.enterprise.endpoints.connectors.fleet.controller
1import re 2from typing import Any 3 4from django.db import transaction 5from requests import RequestException 6from rest_framework.exceptions import ValidationError 7 8from authentik.core.models import User 9from authentik.endpoints.controller import BaseController, Capabilities, ConnectorSyncException 10from authentik.endpoints.facts import ( 11 DeviceFacts, 12 OSFamily, 13) 14from authentik.endpoints.models import ( 15 Device, 16 DeviceAccessGroup, 17 DeviceConnection, 18 DeviceUserBinding, 19) 20from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector as DBC 21from authentik.events.utils import sanitize_item 22from authentik.lib.utils.http import get_http_session 23from authentik.policies.utils import delete_none_values 24 25 26class FleetController(BaseController[DBC]): 27 def __init__(self, *args, **kwargs): 28 super().__init__(*args, **kwargs) 29 self._session = get_http_session() 30 self._session.headers["Authorization"] = f"Bearer {self.connector.token}" 31 if self.connector.headers_mapping: 32 self._session.headers.update( 33 sanitize_item( 34 self.connector.headers_mapping.evaluate( 35 user=None, 36 request=None, 37 connector=self.connector, 38 ) 39 ) 40 ) 41 42 @staticmethod 43 def vendor_identifier() -> str: 44 return "fleetdm.com" 45 46 def capabilities(self) -> list[Capabilities]: 47 return [Capabilities.ENROLL_AUTOMATIC_API] 48 49 def _url(self, path: str) -> str: 50 return f"{self.connector.url}{path}" 51 52 def _paginate_hosts(self): 53 try: 54 page = 0 55 while True: 56 self.logger.info("Fetching page of hosts...", page=page) 57 res = self._session.get( 58 self._url("/api/v1/fleet/hosts"), 59 params={ 60 "order_key": "hardware_serial", 61 "page": page, 62 "per_page": 50, 63 "device_mapping": "true", 64 "populate_software": "true", 65 "populate_users": "true", 66 }, 67 ) 68 res.raise_for_status() 69 hosts: list[dict[str, Any]] = res.json()["hosts"] 70 if len(hosts) < 1: 71 self.logger.info("No more hosts, finished") 72 break 73 self.logger.info("Got hosts", count=len(hosts)) 74 yield from hosts 75 page += 1 76 except RequestException as exc: 77 raise ConnectorSyncException(exc) from exc 78 79 @transaction.atomic 80 def sync_endpoints(self) -> None: 81 for host in self._paginate_hosts(): 82 serial = host["hardware_serial"] 83 device, _ = Device.objects.get_or_create( 84 identifier=serial, defaults={"name": host["hostname"], "expiring": False} 85 ) 86 connection, _ = DeviceConnection.objects.update_or_create( 87 device=device, 88 connector=self.connector, 89 ) 90 if self.connector.map_users: 91 self.map_users(host, device) 92 if self.connector.map_teams_access_group: 93 self.map_access_group(host, device) 94 try: 95 connection.create_snapshot(self.convert_host_data(host)) 96 except ValidationError as exc: 97 self.logger.warning( 98 "failed to create snapshot for host", host=host["hostname"], exc=exc 99 ) 100 101 def map_users(self, host: dict[str, Any], device: Device): 102 for raw_user in host.get("device_mapping", []) or []: 103 user = User.objects.filter(email=raw_user["email"]).first() 104 if not user: 105 continue 106 DeviceUserBinding.objects.update_or_create( 107 target=device, 108 user=user, 109 create_defaults={ 110 "is_primary": True, 111 "order": 0, 112 }, 113 ) 114 115 def map_access_group(self, host: dict[str, Any], device: Device): 116 team_name = host.get("team_name") 117 if not team_name: 118 return 119 group, _ = DeviceAccessGroup.objects.get_or_create(name=team_name) 120 group.attributes["io.goauthentik.endpoints.connectors.fleet.team_id"] = host["team_id"] 121 if device.access_group: 122 return 123 device.access_group = group 124 device.save() 125 126 @staticmethod 127 def os_family(host: dict[str, Any]) -> OSFamily: 128 if host["platform_like"] in ["debian", "rhel"]: 129 return OSFamily.linux 130 if host["platform_like"] == "windows": 131 return OSFamily.windows 132 if host["platform_like"] == "darwin": 133 return OSFamily.macOS 134 if host["platform"] == "android": 135 return OSFamily.android 136 if host["platform"] in ["ipados", "ios"]: 137 return OSFamily.iOS 138 return OSFamily.other 139 140 def map_os(self, host: dict[str, Any]) -> dict[str, str]: 141 family = FleetController.os_family(host) 142 os = { 143 "arch": self.or_none(host["cpu_type"]), 144 "family": family, 145 "name": self.or_none(host["platform_like"]), 146 "version": self.or_none(host["os_version"]), 147 } 148 if not host["os_version"]: 149 return delete_none_values(os) 150 version = re.search(r"(\d+\.(?:\d+\.?)+)", host["os_version"]) 151 if not version: 152 return delete_none_values(os) 153 os["version"] = host["os_version"][version.start() :].strip() 154 os["name"] = host["os_version"][0 : version.start()].strip() 155 return delete_none_values(os) 156 157 def or_none(self, value) -> Any | None: 158 if value == "": 159 return None 160 return value 161 162 def convert_host_data(self, host: dict[str, Any]) -> dict[str, Any]: 163 """Convert host data from fleet to authentik""" 164 fleet_version = "" 165 for pkg in host.get("software") or []: 166 if pkg["name"] in ["fleet-osquery", "fleet-desktop"]: 167 fleet_version = pkg["version"] 168 data = { 169 "os": self.map_os(host), 170 "disks": [], 171 "network": delete_none_values( 172 {"hostname": self.or_none(host["hostname"]), "interfaces": []} 173 ), 174 "hardware": delete_none_values( 175 { 176 "model": self.or_none(host["hardware_model"]), 177 "manufacturer": self.or_none(host["hardware_vendor"]), 178 "serial": self.or_none(host["hardware_serial"]), 179 "cpu_name": self.or_none(host["cpu_brand"]), 180 "cpu_count": self.or_none(host["cpu_logical_cores"]), 181 "memory_bytes": self.or_none(host["memory"]), 182 } 183 ), 184 "software": [ 185 delete_none_values( 186 { 187 "name": x["name"], 188 "version": x["version"], 189 "source": x["source"], 190 } 191 ) 192 for x in (host.get("software") or []) 193 ], 194 "vendor": { 195 "fleetdm.com": { 196 "policies": [ 197 delete_none_values({"name": policy["name"], "status": policy["response"]}) 198 for policy in host.get("policies", []) 199 ], 200 "agent_version": fleet_version, 201 }, 202 }, 203 } 204 facts = DeviceFacts(data=data) 205 facts.is_valid(raise_exception=True) 206 return facts.validated_data
class
FleetController(authentik.endpoints.controller.BaseController[authentik.enterprise.endpoints.connectors.fleet.models.FleetConnector]):
27class FleetController(BaseController[DBC]): 28 def __init__(self, *args, **kwargs): 29 super().__init__(*args, **kwargs) 30 self._session = get_http_session() 31 self._session.headers["Authorization"] = f"Bearer {self.connector.token}" 32 if self.connector.headers_mapping: 33 self._session.headers.update( 34 sanitize_item( 35 self.connector.headers_mapping.evaluate( 36 user=None, 37 request=None, 38 connector=self.connector, 39 ) 40 ) 41 ) 42 43 @staticmethod 44 def vendor_identifier() -> str: 45 return "fleetdm.com" 46 47 def capabilities(self) -> list[Capabilities]: 48 return [Capabilities.ENROLL_AUTOMATIC_API] 49 50 def _url(self, path: str) -> str: 51 return f"{self.connector.url}{path}" 52 53 def _paginate_hosts(self): 54 try: 55 page = 0 56 while True: 57 self.logger.info("Fetching page of hosts...", page=page) 58 res = self._session.get( 59 self._url("/api/v1/fleet/hosts"), 60 params={ 61 "order_key": "hardware_serial", 62 "page": page, 63 "per_page": 50, 64 "device_mapping": "true", 65 "populate_software": "true", 66 "populate_users": "true", 67 }, 68 ) 69 res.raise_for_status() 70 hosts: list[dict[str, Any]] = res.json()["hosts"] 71 if len(hosts) < 1: 72 self.logger.info("No more hosts, finished") 73 break 74 self.logger.info("Got hosts", count=len(hosts)) 75 yield from hosts 76 page += 1 77 except RequestException as exc: 78 raise ConnectorSyncException(exc) from exc 79 80 @transaction.atomic 81 def sync_endpoints(self) -> None: 82 for host in self._paginate_hosts(): 83 serial = host["hardware_serial"] 84 device, _ = Device.objects.get_or_create( 85 identifier=serial, defaults={"name": host["hostname"], "expiring": False} 86 ) 87 connection, _ = DeviceConnection.objects.update_or_create( 88 device=device, 89 connector=self.connector, 90 ) 91 if self.connector.map_users: 92 self.map_users(host, device) 93 if self.connector.map_teams_access_group: 94 self.map_access_group(host, device) 95 try: 96 connection.create_snapshot(self.convert_host_data(host)) 97 except ValidationError as exc: 98 self.logger.warning( 99 "failed to create snapshot for host", host=host["hostname"], exc=exc 100 ) 101 102 def map_users(self, host: dict[str, Any], device: Device): 103 for raw_user in host.get("device_mapping", []) or []: 104 user = User.objects.filter(email=raw_user["email"]).first() 105 if not user: 106 continue 107 DeviceUserBinding.objects.update_or_create( 108 target=device, 109 user=user, 110 create_defaults={ 111 "is_primary": True, 112 "order": 0, 113 }, 114 ) 115 116 def map_access_group(self, host: dict[str, Any], device: Device): 117 team_name = host.get("team_name") 118 if not team_name: 119 return 120 group, _ = DeviceAccessGroup.objects.get_or_create(name=team_name) 121 group.attributes["io.goauthentik.endpoints.connectors.fleet.team_id"] = host["team_id"] 122 if device.access_group: 123 return 124 device.access_group = group 125 device.save() 126 127 @staticmethod 128 def os_family(host: dict[str, Any]) -> OSFamily: 129 if host["platform_like"] in ["debian", "rhel"]: 130 return OSFamily.linux 131 if host["platform_like"] == "windows": 132 return OSFamily.windows 133 if host["platform_like"] == "darwin": 134 return OSFamily.macOS 135 if host["platform"] == "android": 136 return OSFamily.android 137 if host["platform"] in ["ipados", "ios"]: 138 return OSFamily.iOS 139 return OSFamily.other 140 141 def map_os(self, host: dict[str, Any]) -> dict[str, str]: 142 family = FleetController.os_family(host) 143 os = { 144 "arch": self.or_none(host["cpu_type"]), 145 "family": family, 146 "name": self.or_none(host["platform_like"]), 147 "version": self.or_none(host["os_version"]), 148 } 149 if not host["os_version"]: 150 return delete_none_values(os) 151 version = re.search(r"(\d+\.(?:\d+\.?)+)", host["os_version"]) 152 if not version: 153 return delete_none_values(os) 154 os["version"] = host["os_version"][version.start() :].strip() 155 os["name"] = host["os_version"][0 : version.start()].strip() 156 return delete_none_values(os) 157 158 def or_none(self, value) -> Any | None: 159 if value == "": 160 return None 161 return value 162 163 def convert_host_data(self, host: dict[str, Any]) -> dict[str, Any]: 164 """Convert host data from fleet to authentik""" 165 fleet_version = "" 166 for pkg in host.get("software") or []: 167 if pkg["name"] in ["fleet-osquery", "fleet-desktop"]: 168 fleet_version = pkg["version"] 169 data = { 170 "os": self.map_os(host), 171 "disks": [], 172 "network": delete_none_values( 173 {"hostname": self.or_none(host["hostname"]), "interfaces": []} 174 ), 175 "hardware": delete_none_values( 176 { 177 "model": self.or_none(host["hardware_model"]), 178 "manufacturer": self.or_none(host["hardware_vendor"]), 179 "serial": self.or_none(host["hardware_serial"]), 180 "cpu_name": self.or_none(host["cpu_brand"]), 181 "cpu_count": self.or_none(host["cpu_logical_cores"]), 182 "memory_bytes": self.or_none(host["memory"]), 183 } 184 ), 185 "software": [ 186 delete_none_values( 187 { 188 "name": x["name"], 189 "version": x["version"], 190 "source": x["source"], 191 } 192 ) 193 for x in (host.get("software") or []) 194 ], 195 "vendor": { 196 "fleetdm.com": { 197 "policies": [ 198 delete_none_values({"name": policy["name"], "status": policy["response"]}) 199 for policy in host.get("policies", []) 200 ], 201 "agent_version": fleet_version, 202 }, 203 }, 204 } 205 facts = DeviceFacts(data=data) 206 facts.is_valid(raise_exception=True) 207 return facts.validated_data
Abstract base class for generic types.
On Python 3.12 and newer, generic classes implicitly inherit from Generic when they declare a parameter list after the class's name::
class Mapping[KT, VT]:
def __getitem__(self, key: KT) -> VT:
...
# Etc.
On older versions of Python, however, generic classes have to explicitly inherit from Generic.
After a class has been declared to be generic, it can then be used as follows::
def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
try:
return mapping[key]
except KeyError:
return default
FleetController(*args, **kwargs)
28 def __init__(self, *args, **kwargs): 29 super().__init__(*args, **kwargs) 30 self._session = get_http_session() 31 self._session.headers["Authorization"] = f"Bearer {self.connector.token}" 32 if self.connector.headers_mapping: 33 self._session.headers.update( 34 sanitize_item( 35 self.connector.headers_mapping.evaluate( 36 user=None, 37 request=None, 38 connector=self.connector, 39 ) 40 ) 41 )
@transaction.atomic
def
sync_endpoints(self) -> None:
80 @transaction.atomic 81 def sync_endpoints(self) -> None: 82 for host in self._paginate_hosts(): 83 serial = host["hardware_serial"] 84 device, _ = Device.objects.get_or_create( 85 identifier=serial, defaults={"name": host["hostname"], "expiring": False} 86 ) 87 connection, _ = DeviceConnection.objects.update_or_create( 88 device=device, 89 connector=self.connector, 90 ) 91 if self.connector.map_users: 92 self.map_users(host, device) 93 if self.connector.map_teams_access_group: 94 self.map_access_group(host, device) 95 try: 96 connection.create_snapshot(self.convert_host_data(host)) 97 except ValidationError as exc: 98 self.logger.warning( 99 "failed to create snapshot for host", host=host["hostname"], exc=exc 100 )
102 def map_users(self, host: dict[str, Any], device: Device): 103 for raw_user in host.get("device_mapping", []) or []: 104 user = User.objects.filter(email=raw_user["email"]).first() 105 if not user: 106 continue 107 DeviceUserBinding.objects.update_or_create( 108 target=device, 109 user=user, 110 create_defaults={ 111 "is_primary": True, 112 "order": 0, 113 }, 114 )
def
map_access_group( self, host: dict[str, typing.Any], device: authentik.endpoints.models.Device):
116 def map_access_group(self, host: dict[str, Any], device: Device): 117 team_name = host.get("team_name") 118 if not team_name: 119 return 120 group, _ = DeviceAccessGroup.objects.get_or_create(name=team_name) 121 group.attributes["io.goauthentik.endpoints.connectors.fleet.team_id"] = host["team_id"] 122 if device.access_group: 123 return 124 device.access_group = group 125 device.save()
127 @staticmethod 128 def os_family(host: dict[str, Any]) -> OSFamily: 129 if host["platform_like"] in ["debian", "rhel"]: 130 return OSFamily.linux 131 if host["platform_like"] == "windows": 132 return OSFamily.windows 133 if host["platform_like"] == "darwin": 134 return OSFamily.macOS 135 if host["platform"] == "android": 136 return OSFamily.android 137 if host["platform"] in ["ipados", "ios"]: 138 return OSFamily.iOS 139 return OSFamily.other
def
map_os(self, host: dict[str, typing.Any]) -> dict[str, str]:
141 def map_os(self, host: dict[str, Any]) -> dict[str, str]: 142 family = FleetController.os_family(host) 143 os = { 144 "arch": self.or_none(host["cpu_type"]), 145 "family": family, 146 "name": self.or_none(host["platform_like"]), 147 "version": self.or_none(host["os_version"]), 148 } 149 if not host["os_version"]: 150 return delete_none_values(os) 151 version = re.search(r"(\d+\.(?:\d+\.?)+)", host["os_version"]) 152 if not version: 153 return delete_none_values(os) 154 os["version"] = host["os_version"][version.start() :].strip() 155 os["name"] = host["os_version"][0 : version.start()].strip() 156 return delete_none_values(os)
def
convert_host_data(self, host: dict[str, typing.Any]) -> dict[str, typing.Any]:
163 def convert_host_data(self, host: dict[str, Any]) -> dict[str, Any]: 164 """Convert host data from fleet to authentik""" 165 fleet_version = "" 166 for pkg in host.get("software") or []: 167 if pkg["name"] in ["fleet-osquery", "fleet-desktop"]: 168 fleet_version = pkg["version"] 169 data = { 170 "os": self.map_os(host), 171 "disks": [], 172 "network": delete_none_values( 173 {"hostname": self.or_none(host["hostname"]), "interfaces": []} 174 ), 175 "hardware": delete_none_values( 176 { 177 "model": self.or_none(host["hardware_model"]), 178 "manufacturer": self.or_none(host["hardware_vendor"]), 179 "serial": self.or_none(host["hardware_serial"]), 180 "cpu_name": self.or_none(host["cpu_brand"]), 181 "cpu_count": self.or_none(host["cpu_logical_cores"]), 182 "memory_bytes": self.or_none(host["memory"]), 183 } 184 ), 185 "software": [ 186 delete_none_values( 187 { 188 "name": x["name"], 189 "version": x["version"], 190 "source": x["source"], 191 } 192 ) 193 for x in (host.get("software") or []) 194 ], 195 "vendor": { 196 "fleetdm.com": { 197 "policies": [ 198 delete_none_values({"name": policy["name"], "status": policy["response"]}) 199 for policy in host.get("policies", []) 200 ], 201 "agent_version": fleet_version, 202 }, 203 }, 204 } 205 facts = DeviceFacts(data=data) 206 facts.is_valid(raise_exception=True) 207 return facts.validated_data
Convert host data from fleet to authentik