authentik.outposts.models
Outpost models
1"""Outpost models""" 2 3from collections.abc import Iterable 4from dataclasses import asdict, dataclass, field 5from datetime import datetime 6from typing import Any 7from uuid import uuid4 8 9from dacite.core import from_dict 10from django.contrib.auth.models import Permission 11from django.core.cache import cache 12from django.db import IntegrityError, models, transaction 13from django.db.models.base import Model 14from django.utils.translation import gettext_lazy as _ 15from model_utils.managers import InheritanceManager 16from packaging.version import Version, parse 17from rest_framework.serializers import Serializer 18from structlog.stdlib import get_logger 19 20from authentik import authentik_build_hash, authentik_version 21from authentik.blueprints.models import ManagedModel 22from authentik.brands.models import Brand 23from authentik.core.models import ( 24 USER_PATH_SYSTEM_PREFIX, 25 Provider, 26 Token, 27 TokenIntents, 28 User, 29 UserTypes, 30) 31from authentik.crypto.models import CertificateKeyPair 32from authentik.events.models import Event, EventAction 33from authentik.lib.config import CONFIG 34from authentik.lib.models import InheritanceForeignKey, SerializerModel 35from authentik.lib.sentry import SentryIgnoredException 36from authentik.lib.utils.time import fqdn_rand 37from authentik.outposts.controllers.k8s.utils import get_namespace 38from authentik.tasks.schedules.common import ScheduleSpec 39from authentik.tasks.schedules.models import ScheduledModel 40 41OUR_VERSION = parse(authentik_version()) 42OUTPOST_HELLO_INTERVAL = 10 43LOGGER = get_logger() 44 45USER_PATH_OUTPOSTS = USER_PATH_SYSTEM_PREFIX + "/outposts" 46 47 48class ServiceConnectionInvalid(SentryIgnoredException): 49 """Exception raised when a Service Connection has invalid parameters""" 50 51 52@dataclass 53class OutpostConfig: 54 """Configuration an outpost uses to configure it self""" 55 56 # update website/docs/add-secure-apps/outposts/_config.md 57 58 authentik_host: str = "" 59 authentik_host_insecure: bool = False 60 authentik_host_browser: str = "" 61 62 log_level: str = CONFIG.get("log_level") 63 object_naming_template: str = field(default="ak-outpost-%(name)s") 64 refresh_interval: str = "minutes=5" 65 66 container_image: str | None = field(default=None) 67 68 docker_network: str | None = field(default=None) 69 docker_map_ports: bool = field(default=True) 70 docker_labels: dict[str, str] | None = field(default=None) 71 72 kubernetes_replicas: int = field(default=1) 73 kubernetes_namespace: str = field(default_factory=get_namespace) 74 kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) 75 kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls") 76 kubernetes_ingress_class_name: str | None = field(default=None) 77 kubernetes_ingress_path_type: str | None = field(default=None) 78 kubernetes_httproute_annotations: dict[str, str] = field(default_factory=dict) 79 kubernetes_httproute_parent_refs: list[dict[str, str]] = field(default_factory=list) 80 kubernetes_service_type: str = field(default="ClusterIP") 81 kubernetes_disabled_components: list[str] = field(default_factory=list) 82 kubernetes_image_pull_secrets: list[str] = field(default_factory=list) 83 kubernetes_json_patches: dict[str, list[dict[str, Any]]] | None = field(default=None) 84 85 86class OutpostModel(Model): 87 """Base model for providers that need more objects than just themselves""" 88 89 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 90 """Return a list of all required objects""" 91 return [self] 92 93 class Meta: 94 abstract = True 95 96 97class OutpostType(models.TextChoices): 98 """Outpost types""" 99 100 PROXY = "proxy" 101 LDAP = "ldap" 102 RADIUS = "radius" 103 RAC = "rac" 104 105 106def default_outpost_config(host: str | None = None): 107 """Get default outpost config""" 108 return asdict(OutpostConfig(authentik_host=host or "")) 109 110 111@dataclass 112class OutpostServiceConnectionState: 113 """State of an Outpost Service Connection""" 114 115 version: str 116 healthy: bool 117 118 119class OutpostServiceConnection(ScheduledModel, models.Model): 120 """Connection details for an Outpost Controller, like Docker or Kubernetes""" 121 122 uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) 123 name = models.TextField(unique=True) 124 125 local = models.BooleanField( 126 default=False, 127 help_text=_( 128 "If enabled, use the local connection. Required Docker socket/Kubernetes Integration" 129 ), 130 ) 131 132 objects = InheritanceManager() 133 134 class Meta: 135 verbose_name = _("Outpost Service-Connection") 136 verbose_name_plural = _("Outpost Service-Connections") 137 138 def __str__(self) -> str: 139 return f"Outpost service connection {self.name}" 140 141 @property 142 def state_key(self) -> str: 143 """Key used to save connection state in cache""" 144 return f"goauthentik.io/outposts/service_connection_state/{self.pk.hex}" 145 146 @property 147 def state(self) -> OutpostServiceConnectionState: 148 """Get state of service connection""" 149 from authentik.outposts.tasks import outpost_service_connection_monitor 150 151 state = cache.get(self.state_key, None) 152 if not state: 153 outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self) 154 return OutpostServiceConnectionState("", False) 155 return state 156 157 @property 158 def component(self) -> str: 159 """Return component used to edit this object""" 160 # This is called when creating an outpost with a service connection 161 # since the response doesn't use the correct inheritance 162 return "" 163 164 @property 165 def schedule_specs(self) -> list[ScheduleSpec]: 166 from authentik.outposts.tasks import outpost_service_connection_monitor 167 168 return [ 169 ScheduleSpec( 170 actor=outpost_service_connection_monitor, 171 uid=self.name, 172 args=(self.pk,), 173 crontab="3-59/15 * * * *", 174 send_on_save=True, 175 ), 176 ] 177 178 179class DockerServiceConnection(SerializerModel, OutpostServiceConnection): 180 """Service Connection to a Docker endpoint""" 181 182 url = models.TextField( 183 help_text=_( 184 "Can be in the format of 'unix://<path>' when connecting to a local docker daemon, " 185 "or 'https://<hostname>:2376' when connecting to a remote system." 186 ) 187 ) 188 tls_verification = models.ForeignKey( 189 CertificateKeyPair, 190 null=True, 191 blank=True, 192 default=None, 193 related_name="+", 194 on_delete=models.SET_DEFAULT, 195 help_text=_( 196 "CA which the endpoint's Certificate is verified against. " 197 "Can be left empty for no validation." 198 ), 199 ) 200 tls_authentication = models.ForeignKey( 201 CertificateKeyPair, 202 null=True, 203 blank=True, 204 default=None, 205 related_name="+", 206 on_delete=models.SET_DEFAULT, 207 help_text=_( 208 "Certificate/Key used for authentication. Can be left empty for no authentication." 209 ), 210 ) 211 212 class Meta: 213 verbose_name = _("Docker Service-Connection") 214 verbose_name_plural = _("Docker Service-Connections") 215 216 def __str__(self) -> str: 217 return f"Docker Service-Connection {self.name}" 218 219 @property 220 def serializer(self) -> Serializer: 221 from authentik.outposts.api.service_connections import DockerServiceConnectionSerializer 222 223 return DockerServiceConnectionSerializer 224 225 @property 226 def component(self) -> str: 227 return "ak-service-connection-docker-form" 228 229 230class KubernetesServiceConnection(SerializerModel, OutpostServiceConnection): 231 """Service Connection to a Kubernetes cluster""" 232 233 kubeconfig = models.JSONField( 234 help_text=_( 235 "Paste your kubeconfig here. authentik will automatically use " 236 "the currently selected context." 237 ), 238 blank=True, 239 ) 240 verify_ssl = models.BooleanField( 241 default=True, help_text=_("Verify SSL Certificates of the Kubernetes API endpoint") 242 ) 243 244 class Meta: 245 verbose_name = _("Kubernetes Service-Connection") 246 verbose_name_plural = _("Kubernetes Service-Connections") 247 248 def __str__(self) -> str: 249 return f"Kubernetes Service-Connection {self.name}" 250 251 @property 252 def serializer(self) -> Serializer: 253 from authentik.outposts.api.service_connections import KubernetesServiceConnectionSerializer 254 255 return KubernetesServiceConnectionSerializer 256 257 @property 258 def component(self) -> str: 259 return "ak-service-connection-kubernetes-form" 260 261 262class Outpost(ScheduledModel, SerializerModel, ManagedModel): 263 """Outpost instance which manages a service user and token""" 264 265 uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) 266 name = models.TextField(unique=True) 267 268 type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY) 269 service_connection = InheritanceForeignKey( 270 OutpostServiceConnection, 271 default=None, 272 null=True, 273 blank=True, 274 help_text=_( 275 "Select Service-Connection authentik should use to manage this outpost. " 276 "Leave empty if authentik should not handle the deployment." 277 ), 278 on_delete=models.SET_DEFAULT, 279 ) 280 281 _config = models.JSONField(default=default_outpost_config) 282 283 providers = models.ManyToManyField(Provider) 284 285 @property 286 def serializer(self) -> Serializer: 287 from authentik.outposts.api.outposts import OutpostSerializer 288 289 return OutpostSerializer 290 291 @property 292 def config(self) -> OutpostConfig: 293 """Load config as OutpostConfig object""" 294 return from_dict(OutpostConfig, self._config) 295 296 @config.setter 297 def config(self, value): 298 """Dump config into json""" 299 self._config = asdict(value) 300 301 @property 302 def state_cache_prefix(self) -> str: 303 """Key by which the outposts status is saved""" 304 return f"goauthentik.io/outposts/state/{self.uuid.hex}" 305 306 @property 307 def state(self) -> list[OutpostState]: 308 """Get outpost's health status""" 309 return OutpostState.for_outpost(self) 310 311 @property 312 def user_identifier(self): 313 """Username for service user""" 314 return f"ak-outpost-{self.uuid.hex}" 315 316 @property 317 def schedule_specs(self) -> list[ScheduleSpec]: 318 from authentik.outposts.tasks import outpost_controller 319 320 return [ 321 ScheduleSpec( 322 actor=outpost_controller, 323 uid=self.name, 324 args=(self.pk,), 325 kwargs={"action": "up", "from_cache": False}, 326 crontab=f"{fqdn_rand('outpost_controller')} */4 * * *", 327 send_on_save=True, 328 ), 329 ] 330 331 def build_user_permissions(self, user: User): 332 """Create per-object and global permissions for outpost service-account""" 333 # To ensure the user only has the correct permissions, we delete all of them and re-add 334 # the ones the user needs 335 try: 336 with transaction.atomic(): 337 user.remove_all_perms_from_managed_role() 338 for model_or_perm in self.get_required_objects(): 339 if isinstance(model_or_perm, models.Model): 340 code_name = ( 341 f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}" 342 ) 343 user.assign_perms_to_managed_role(code_name, model_or_perm) 344 elif isinstance(model_or_perm, tuple): 345 perm, obj = model_or_perm 346 user.assign_perms_to_managed_role(perm, obj) 347 else: 348 user.assign_perms_to_managed_role(model_or_perm) 349 except (Permission.DoesNotExist, AttributeError) as exc: 350 LOGGER.warning( 351 "permission doesn't exist", 352 code_name=code_name, 353 user=user, 354 model=model_or_perm, 355 ) 356 Event.new( 357 action=EventAction.SYSTEM_EXCEPTION, 358 message=( 359 "While setting the permissions for the service-account, a " 360 "permission was not found: Check " 361 "https://docs.goauthentik.io/troubleshooting/missing_permission" 362 ), 363 ).with_exception(exc).set_user(user).save() 364 LOGGER.debug( 365 "Updated service account's permissions", 366 obj_perms=user.get_all_obj_perms_on_managed_role(), 367 perms=user.get_all_model_perms_on_managed_role(), 368 ) 369 370 @property 371 def user(self) -> User: 372 """Get/create user with access to all required objects""" 373 user = User.objects.filter(username=self.user_identifier).first() 374 user_created = False 375 if not user: 376 user: User = User.objects.create(username=self.user_identifier) 377 user_created = True 378 attrs = { 379 "type": UserTypes.INTERNAL_SERVICE_ACCOUNT, 380 "name": f"Outpost {self.name} Service-Account", 381 "path": USER_PATH_OUTPOSTS, 382 } 383 dirty = False 384 for key, value in attrs.items(): 385 if getattr(user, key) != value: 386 dirty = True 387 setattr(user, key, value) 388 if user.has_usable_password(): 389 user.set_unusable_password() 390 dirty = True 391 if dirty: 392 user.save() 393 if user_created: 394 self.build_user_permissions(user) 395 return user 396 397 @property 398 def token_identifier(self) -> str: 399 """Get Token identifier""" 400 return f"ak-outpost-{self.pk}-api" 401 402 @property 403 def token(self) -> Token: 404 """Get/create token for auto-generated user""" 405 managed = f"goauthentik.io/outpost/{self.token_identifier}" 406 tokens = Token.objects.filter( 407 identifier=self.token_identifier, 408 intent=TokenIntents.INTENT_API, 409 managed=managed, 410 ) 411 if tokens.exists(): 412 return tokens.first() 413 try: 414 return Token.objects.create( 415 user=self.user, 416 identifier=self.token_identifier, 417 intent=TokenIntents.INTENT_API, 418 description=f"Autogenerated by authentik for Outpost {self.name}", 419 expiring=False, 420 managed=managed, 421 ) 422 except IntegrityError: 423 # Integrity error happens mostly when managed is reused 424 Token.objects.filter(managed=managed).delete() 425 Token.objects.filter(identifier=self.token_identifier).delete() 426 return self.token 427 428 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 429 """Get an iterator of all objects the user needs read access to""" 430 objects: list[models.Model | str] = [ 431 self, 432 "authentik_events.add_event", 433 ] 434 for provider in Provider.objects.filter(outpost=self).select_related().select_subclasses(): 435 if isinstance(provider, OutpostModel): 436 objects.extend(provider.get_required_objects()) 437 else: 438 objects.append(provider) 439 if self.managed: 440 for brand in Brand.objects.filter(web_certificate__isnull=False): 441 objects.append(brand) 442 objects.append(("authentik_crypto.view_certificatekeypair", brand.web_certificate)) 443 objects.append( 444 ("authentik_crypto.view_certificatekeypair_certificate", brand.web_certificate) 445 ) 446 objects.append( 447 ("authentik_crypto.view_certificatekeypair_key", brand.web_certificate) 448 ) 449 return objects 450 451 def __str__(self) -> str: 452 return f"Outpost {self.name}" 453 454 class Meta: 455 verbose_name = _("Outpost") 456 verbose_name_plural = _("Outposts") 457 458 459@dataclass 460class OutpostState: 461 """Outpost instance state, last_seen and version""" 462 463 uid: str 464 last_seen: datetime | None = field(default=None) 465 version: str | None = field(default=None) 466 version_should: Version = field(default=OUR_VERSION) 467 build_hash: str = field(default="") 468 golang_version: str = field(default="") 469 openssl_enabled: bool = field(default=False) 470 openssl_version: str = field(default="") 471 fips_enabled: bool = field(default=False) 472 hostname: str = field(default="") 473 args: dict = field(default_factory=dict) 474 475 _outpost: Outpost | None = field(default=None) 476 477 @property 478 def version_outdated(self) -> bool: 479 """Check if outpost version matches our version""" 480 if not self.version: 481 return False 482 if self.build_hash != authentik_build_hash(): 483 return False 484 return parse(self.version) != OUR_VERSION 485 486 @staticmethod 487 def for_outpost(outpost: Outpost) -> list[OutpostState]: 488 """Get all states for an outpost""" 489 keys = cache.keys(f"{outpost.state_cache_prefix}/*") 490 if not keys: 491 return [] 492 states = [] 493 for key in keys: 494 instance_uid = key.replace(f"{outpost.state_cache_prefix}/", "") 495 states.append(OutpostState.for_instance_uid(outpost, instance_uid)) 496 return states 497 498 @staticmethod 499 def for_instance_uid(outpost: Outpost, uid: str) -> OutpostState: 500 """Get state for a single instance""" 501 key = f"{outpost.state_cache_prefix}/{uid}" 502 default_data = {"uid": uid} 503 data = cache.get(key, default_data) 504 if isinstance(data, str): 505 cache.delete(key) 506 data = default_data 507 state = from_dict(OutpostState, data) 508 509 state._outpost = outpost 510 return state 511 512 def save(self, timeout=OUTPOST_HELLO_INTERVAL): 513 """Save current state to cache""" 514 full_key = f"{self._outpost.state_cache_prefix}/{self.uid}" 515 return cache.set(full_key, asdict(self), timeout=timeout) 516 517 def delete(self): 518 """Manually delete from cache, used on channel disconnect""" 519 full_key = f"{self._outpost.state_cache_prefix}/{self.uid}" 520 cache.delete(full_key)
49class ServiceConnectionInvalid(SentryIgnoredException): 50 """Exception raised when a Service Connection has invalid parameters"""
Exception raised when a Service Connection has invalid parameters
53@dataclass 54class OutpostConfig: 55 """Configuration an outpost uses to configure it self""" 56 57 # update website/docs/add-secure-apps/outposts/_config.md 58 59 authentik_host: str = "" 60 authentik_host_insecure: bool = False 61 authentik_host_browser: str = "" 62 63 log_level: str = CONFIG.get("log_level") 64 object_naming_template: str = field(default="ak-outpost-%(name)s") 65 refresh_interval: str = "minutes=5" 66 67 container_image: str | None = field(default=None) 68 69 docker_network: str | None = field(default=None) 70 docker_map_ports: bool = field(default=True) 71 docker_labels: dict[str, str] | None = field(default=None) 72 73 kubernetes_replicas: int = field(default=1) 74 kubernetes_namespace: str = field(default_factory=get_namespace) 75 kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) 76 kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls") 77 kubernetes_ingress_class_name: str | None = field(default=None) 78 kubernetes_ingress_path_type: str | None = field(default=None) 79 kubernetes_httproute_annotations: dict[str, str] = field(default_factory=dict) 80 kubernetes_httproute_parent_refs: list[dict[str, str]] = field(default_factory=list) 81 kubernetes_service_type: str = field(default="ClusterIP") 82 kubernetes_disabled_components: list[str] = field(default_factory=list) 83 kubernetes_image_pull_secrets: list[str] = field(default_factory=list) 84 kubernetes_json_patches: dict[str, list[dict[str, Any]]] | None = field(default=None)
Configuration an outpost uses to configure it self
87class OutpostModel(Model): 88 """Base model for providers that need more objects than just themselves""" 89 90 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 91 """Return a list of all required objects""" 92 return [self] 93 94 class Meta: 95 abstract = True
Base model for providers that need more objects than just themselves
90 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 91 """Return a list of all required objects""" 92 return [self]
Return a list of all required objects
98class OutpostType(models.TextChoices): 99 """Outpost types""" 100 101 PROXY = "proxy" 102 LDAP = "ldap" 103 RADIUS = "radius" 104 RAC = "rac"
Outpost types
107def default_outpost_config(host: str | None = None): 108 """Get default outpost config""" 109 return asdict(OutpostConfig(authentik_host=host or ""))
Get default outpost config
112@dataclass 113class OutpostServiceConnectionState: 114 """State of an Outpost Service Connection""" 115 116 version: str 117 healthy: bool
State of an Outpost Service Connection
120class OutpostServiceConnection(ScheduledModel, models.Model): 121 """Connection details for an Outpost Controller, like Docker or Kubernetes""" 122 123 uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) 124 name = models.TextField(unique=True) 125 126 local = models.BooleanField( 127 default=False, 128 help_text=_( 129 "If enabled, use the local connection. Required Docker socket/Kubernetes Integration" 130 ), 131 ) 132 133 objects = InheritanceManager() 134 135 class Meta: 136 verbose_name = _("Outpost Service-Connection") 137 verbose_name_plural = _("Outpost Service-Connections") 138 139 def __str__(self) -> str: 140 return f"Outpost service connection {self.name}" 141 142 @property 143 def state_key(self) -> str: 144 """Key used to save connection state in cache""" 145 return f"goauthentik.io/outposts/service_connection_state/{self.pk.hex}" 146 147 @property 148 def state(self) -> OutpostServiceConnectionState: 149 """Get state of service connection""" 150 from authentik.outposts.tasks import outpost_service_connection_monitor 151 152 state = cache.get(self.state_key, None) 153 if not state: 154 outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self) 155 return OutpostServiceConnectionState("", False) 156 return state 157 158 @property 159 def component(self) -> str: 160 """Return component used to edit this object""" 161 # This is called when creating an outpost with a service connection 162 # since the response doesn't use the correct inheritance 163 return "" 164 165 @property 166 def schedule_specs(self) -> list[ScheduleSpec]: 167 from authentik.outposts.tasks import outpost_service_connection_monitor 168 169 return [ 170 ScheduleSpec( 171 actor=outpost_service_connection_monitor, 172 uid=self.name, 173 args=(self.pk,), 174 crontab="3-59/15 * * * *", 175 send_on_save=True, 176 ), 177 ]
Connection details for an Outpost Controller, like Docker or Kubernetes
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
142 @property 143 def state_key(self) -> str: 144 """Key used to save connection state in cache""" 145 return f"goauthentik.io/outposts/service_connection_state/{self.pk.hex}"
Key used to save connection state in cache
147 @property 148 def state(self) -> OutpostServiceConnectionState: 149 """Get state of service connection""" 150 from authentik.outposts.tasks import outpost_service_connection_monitor 151 152 state = cache.get(self.state_key, None) 153 if not state: 154 outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self) 155 return OutpostServiceConnectionState("", False) 156 return state
Get state of service connection
158 @property 159 def component(self) -> str: 160 """Return component used to edit this object""" 161 # This is called when creating an outpost with a service connection 162 # since the response doesn't use the correct inheritance 163 return ""
Return component used to edit this object
165 @property 166 def schedule_specs(self) -> list[ScheduleSpec]: 167 from authentik.outposts.tasks import outpost_service_connection_monitor 168 169 return [ 170 ScheduleSpec( 171 actor=outpost_service_connection_monitor, 172 uid=self.name, 173 args=(self.pk,), 174 crontab="3-59/15 * * * *", 175 send_on_save=True, 176 ), 177 ]
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
Accessor to the related object on the reverse side of a one-to-one relation.
In the example::
class Restaurant(Model):
place = OneToOneField(Place, related_name='restaurant')
Place.restaurant is a ReverseOneToOneDescriptor instance.
Accessor to the related object on the reverse side of a one-to-one relation.
In the example::
class Restaurant(Model):
place = OneToOneField(Place, related_name='restaurant')
Place.restaurant is a ReverseOneToOneDescriptor instance.
Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Parent.children is a ReverseManyToOneDescriptor instance.
Most of the implementation is delegated to a dynamically defined manager
class built by create_forward_many_to_many_manager() defined below.
Inherited Members
The requested object does not exist
The query returned multiple objects when only one was expected.
180class DockerServiceConnection(SerializerModel, OutpostServiceConnection): 181 """Service Connection to a Docker endpoint""" 182 183 url = models.TextField( 184 help_text=_( 185 "Can be in the format of 'unix://<path>' when connecting to a local docker daemon, " 186 "or 'https://<hostname>:2376' when connecting to a remote system." 187 ) 188 ) 189 tls_verification = models.ForeignKey( 190 CertificateKeyPair, 191 null=True, 192 blank=True, 193 default=None, 194 related_name="+", 195 on_delete=models.SET_DEFAULT, 196 help_text=_( 197 "CA which the endpoint's Certificate is verified against. " 198 "Can be left empty for no validation." 199 ), 200 ) 201 tls_authentication = models.ForeignKey( 202 CertificateKeyPair, 203 null=True, 204 blank=True, 205 default=None, 206 related_name="+", 207 on_delete=models.SET_DEFAULT, 208 help_text=_( 209 "Certificate/Key used for authentication. Can be left empty for no authentication." 210 ), 211 ) 212 213 class Meta: 214 verbose_name = _("Docker Service-Connection") 215 verbose_name_plural = _("Docker Service-Connections") 216 217 def __str__(self) -> str: 218 return f"Docker Service-Connection {self.name}" 219 220 @property 221 def serializer(self) -> Serializer: 222 from authentik.outposts.api.service_connections import DockerServiceConnectionSerializer 223 224 return DockerServiceConnectionSerializer 225 226 @property 227 def component(self) -> str: 228 return "ak-service-connection-docker-form"
Service Connection to a Docker endpoint
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Child.parent is a ForwardManyToOneDescriptor instance.
Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example::
class Child(Model):
parent = ForeignKey(Parent, related_name='children')
Child.parent is a ForwardManyToOneDescriptor instance.
220 @property 221 def serializer(self) -> Serializer: 222 from authentik.outposts.api.service_connections import DockerServiceConnectionSerializer 223 224 return DockerServiceConnectionSerializer
Get serializer for this model
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
The requested object does not exist
The query returned multiple objects when only one was expected.
231class KubernetesServiceConnection(SerializerModel, OutpostServiceConnection): 232 """Service Connection to a Kubernetes cluster""" 233 234 kubeconfig = models.JSONField( 235 help_text=_( 236 "Paste your kubeconfig here. authentik will automatically use " 237 "the currently selected context." 238 ), 239 blank=True, 240 ) 241 verify_ssl = models.BooleanField( 242 default=True, help_text=_("Verify SSL Certificates of the Kubernetes API endpoint") 243 ) 244 245 class Meta: 246 verbose_name = _("Kubernetes Service-Connection") 247 verbose_name_plural = _("Kubernetes Service-Connections") 248 249 def __str__(self) -> str: 250 return f"Kubernetes Service-Connection {self.name}" 251 252 @property 253 def serializer(self) -> Serializer: 254 from authentik.outposts.api.service_connections import KubernetesServiceConnectionSerializer 255 256 return KubernetesServiceConnectionSerializer 257 258 @property 259 def component(self) -> str: 260 return "ak-service-connection-kubernetes-form"
Service Connection to a Kubernetes cluster
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
252 @property 253 def serializer(self) -> Serializer: 254 from authentik.outposts.api.service_connections import KubernetesServiceConnectionSerializer 255 256 return KubernetesServiceConnectionSerializer
Get serializer for this model
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
The requested object does not exist
The query returned multiple objects when only one was expected.
263class Outpost(ScheduledModel, SerializerModel, ManagedModel): 264 """Outpost instance which manages a service user and token""" 265 266 uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) 267 name = models.TextField(unique=True) 268 269 type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY) 270 service_connection = InheritanceForeignKey( 271 OutpostServiceConnection, 272 default=None, 273 null=True, 274 blank=True, 275 help_text=_( 276 "Select Service-Connection authentik should use to manage this outpost. " 277 "Leave empty if authentik should not handle the deployment." 278 ), 279 on_delete=models.SET_DEFAULT, 280 ) 281 282 _config = models.JSONField(default=default_outpost_config) 283 284 providers = models.ManyToManyField(Provider) 285 286 @property 287 def serializer(self) -> Serializer: 288 from authentik.outposts.api.outposts import OutpostSerializer 289 290 return OutpostSerializer 291 292 @property 293 def config(self) -> OutpostConfig: 294 """Load config as OutpostConfig object""" 295 return from_dict(OutpostConfig, self._config) 296 297 @config.setter 298 def config(self, value): 299 """Dump config into json""" 300 self._config = asdict(value) 301 302 @property 303 def state_cache_prefix(self) -> str: 304 """Key by which the outposts status is saved""" 305 return f"goauthentik.io/outposts/state/{self.uuid.hex}" 306 307 @property 308 def state(self) -> list[OutpostState]: 309 """Get outpost's health status""" 310 return OutpostState.for_outpost(self) 311 312 @property 313 def user_identifier(self): 314 """Username for service user""" 315 return f"ak-outpost-{self.uuid.hex}" 316 317 @property 318 def schedule_specs(self) -> list[ScheduleSpec]: 319 from authentik.outposts.tasks import outpost_controller 320 321 return [ 322 ScheduleSpec( 323 actor=outpost_controller, 324 uid=self.name, 325 args=(self.pk,), 326 kwargs={"action": "up", "from_cache": False}, 327 crontab=f"{fqdn_rand('outpost_controller')} */4 * * *", 328 send_on_save=True, 329 ), 330 ] 331 332 def build_user_permissions(self, user: User): 333 """Create per-object and global permissions for outpost service-account""" 334 # To ensure the user only has the correct permissions, we delete all of them and re-add 335 # the ones the user needs 336 try: 337 with transaction.atomic(): 338 user.remove_all_perms_from_managed_role() 339 for model_or_perm in self.get_required_objects(): 340 if isinstance(model_or_perm, models.Model): 341 code_name = ( 342 f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}" 343 ) 344 user.assign_perms_to_managed_role(code_name, model_or_perm) 345 elif isinstance(model_or_perm, tuple): 346 perm, obj = model_or_perm 347 user.assign_perms_to_managed_role(perm, obj) 348 else: 349 user.assign_perms_to_managed_role(model_or_perm) 350 except (Permission.DoesNotExist, AttributeError) as exc: 351 LOGGER.warning( 352 "permission doesn't exist", 353 code_name=code_name, 354 user=user, 355 model=model_or_perm, 356 ) 357 Event.new( 358 action=EventAction.SYSTEM_EXCEPTION, 359 message=( 360 "While setting the permissions for the service-account, a " 361 "permission was not found: Check " 362 "https://docs.goauthentik.io/troubleshooting/missing_permission" 363 ), 364 ).with_exception(exc).set_user(user).save() 365 LOGGER.debug( 366 "Updated service account's permissions", 367 obj_perms=user.get_all_obj_perms_on_managed_role(), 368 perms=user.get_all_model_perms_on_managed_role(), 369 ) 370 371 @property 372 def user(self) -> User: 373 """Get/create user with access to all required objects""" 374 user = User.objects.filter(username=self.user_identifier).first() 375 user_created = False 376 if not user: 377 user: User = User.objects.create(username=self.user_identifier) 378 user_created = True 379 attrs = { 380 "type": UserTypes.INTERNAL_SERVICE_ACCOUNT, 381 "name": f"Outpost {self.name} Service-Account", 382 "path": USER_PATH_OUTPOSTS, 383 } 384 dirty = False 385 for key, value in attrs.items(): 386 if getattr(user, key) != value: 387 dirty = True 388 setattr(user, key, value) 389 if user.has_usable_password(): 390 user.set_unusable_password() 391 dirty = True 392 if dirty: 393 user.save() 394 if user_created: 395 self.build_user_permissions(user) 396 return user 397 398 @property 399 def token_identifier(self) -> str: 400 """Get Token identifier""" 401 return f"ak-outpost-{self.pk}-api" 402 403 @property 404 def token(self) -> Token: 405 """Get/create token for auto-generated user""" 406 managed = f"goauthentik.io/outpost/{self.token_identifier}" 407 tokens = Token.objects.filter( 408 identifier=self.token_identifier, 409 intent=TokenIntents.INTENT_API, 410 managed=managed, 411 ) 412 if tokens.exists(): 413 return tokens.first() 414 try: 415 return Token.objects.create( 416 user=self.user, 417 identifier=self.token_identifier, 418 intent=TokenIntents.INTENT_API, 419 description=f"Autogenerated by authentik for Outpost {self.name}", 420 expiring=False, 421 managed=managed, 422 ) 423 except IntegrityError: 424 # Integrity error happens mostly when managed is reused 425 Token.objects.filter(managed=managed).delete() 426 Token.objects.filter(identifier=self.token_identifier).delete() 427 return self.token 428 429 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 430 """Get an iterator of all objects the user needs read access to""" 431 objects: list[models.Model | str] = [ 432 self, 433 "authentik_events.add_event", 434 ] 435 for provider in Provider.objects.filter(outpost=self).select_related().select_subclasses(): 436 if isinstance(provider, OutpostModel): 437 objects.extend(provider.get_required_objects()) 438 else: 439 objects.append(provider) 440 if self.managed: 441 for brand in Brand.objects.filter(web_certificate__isnull=False): 442 objects.append(brand) 443 objects.append(("authentik_crypto.view_certificatekeypair", brand.web_certificate)) 444 objects.append( 445 ("authentik_crypto.view_certificatekeypair_certificate", brand.web_certificate) 446 ) 447 objects.append( 448 ("authentik_crypto.view_certificatekeypair_key", brand.web_certificate) 449 ) 450 return objects 451 452 def __str__(self) -> str: 453 return f"Outpost {self.name}" 454 455 class Meta: 456 verbose_name = _("Outpost") 457 verbose_name_plural = _("Outposts")
Outpost instance which manages a service user and token
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Forward ManyToOne Descriptor that selects subclass. Requires InheritanceAutoManager.
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example::
class Pizza(Model):
toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings and Topping.pizzas are ManyToManyDescriptor
instances.
Most of the implementation is delegated to a dynamically defined manager
class built by create_forward_many_to_many_manager() defined below.
286 @property 287 def serializer(self) -> Serializer: 288 from authentik.outposts.api.outposts import OutpostSerializer 289 290 return OutpostSerializer
Get serializer for this model
292 @property 293 def config(self) -> OutpostConfig: 294 """Load config as OutpostConfig object""" 295 return from_dict(OutpostConfig, self._config)
Load config as OutpostConfig object
302 @property 303 def state_cache_prefix(self) -> str: 304 """Key by which the outposts status is saved""" 305 return f"goauthentik.io/outposts/state/{self.uuid.hex}"
Key by which the outposts status is saved
307 @property 308 def state(self) -> list[OutpostState]: 309 """Get outpost's health status""" 310 return OutpostState.for_outpost(self)
Get outpost's health status
312 @property 313 def user_identifier(self): 314 """Username for service user""" 315 return f"ak-outpost-{self.uuid.hex}"
Username for service user
317 @property 318 def schedule_specs(self) -> list[ScheduleSpec]: 319 from authentik.outposts.tasks import outpost_controller 320 321 return [ 322 ScheduleSpec( 323 actor=outpost_controller, 324 uid=self.name, 325 args=(self.pk,), 326 kwargs={"action": "up", "from_cache": False}, 327 crontab=f"{fqdn_rand('outpost_controller')} */4 * * *", 328 send_on_save=True, 329 ), 330 ]
332 def build_user_permissions(self, user: User): 333 """Create per-object and global permissions for outpost service-account""" 334 # To ensure the user only has the correct permissions, we delete all of them and re-add 335 # the ones the user needs 336 try: 337 with transaction.atomic(): 338 user.remove_all_perms_from_managed_role() 339 for model_or_perm in self.get_required_objects(): 340 if isinstance(model_or_perm, models.Model): 341 code_name = ( 342 f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}" 343 ) 344 user.assign_perms_to_managed_role(code_name, model_or_perm) 345 elif isinstance(model_or_perm, tuple): 346 perm, obj = model_or_perm 347 user.assign_perms_to_managed_role(perm, obj) 348 else: 349 user.assign_perms_to_managed_role(model_or_perm) 350 except (Permission.DoesNotExist, AttributeError) as exc: 351 LOGGER.warning( 352 "permission doesn't exist", 353 code_name=code_name, 354 user=user, 355 model=model_or_perm, 356 ) 357 Event.new( 358 action=EventAction.SYSTEM_EXCEPTION, 359 message=( 360 "While setting the permissions for the service-account, a " 361 "permission was not found: Check " 362 "https://docs.goauthentik.io/troubleshooting/missing_permission" 363 ), 364 ).with_exception(exc).set_user(user).save() 365 LOGGER.debug( 366 "Updated service account's permissions", 367 obj_perms=user.get_all_obj_perms_on_managed_role(), 368 perms=user.get_all_model_perms_on_managed_role(), 369 )
Create per-object and global permissions for outpost service-account
371 @property 372 def user(self) -> User: 373 """Get/create user with access to all required objects""" 374 user = User.objects.filter(username=self.user_identifier).first() 375 user_created = False 376 if not user: 377 user: User = User.objects.create(username=self.user_identifier) 378 user_created = True 379 attrs = { 380 "type": UserTypes.INTERNAL_SERVICE_ACCOUNT, 381 "name": f"Outpost {self.name} Service-Account", 382 "path": USER_PATH_OUTPOSTS, 383 } 384 dirty = False 385 for key, value in attrs.items(): 386 if getattr(user, key) != value: 387 dirty = True 388 setattr(user, key, value) 389 if user.has_usable_password(): 390 user.set_unusable_password() 391 dirty = True 392 if dirty: 393 user.save() 394 if user_created: 395 self.build_user_permissions(user) 396 return user
Get/create user with access to all required objects
398 @property 399 def token_identifier(self) -> str: 400 """Get Token identifier""" 401 return f"ak-outpost-{self.pk}-api"
Get Token identifier
403 @property 404 def token(self) -> Token: 405 """Get/create token for auto-generated user""" 406 managed = f"goauthentik.io/outpost/{self.token_identifier}" 407 tokens = Token.objects.filter( 408 identifier=self.token_identifier, 409 intent=TokenIntents.INTENT_API, 410 managed=managed, 411 ) 412 if tokens.exists(): 413 return tokens.first() 414 try: 415 return Token.objects.create( 416 user=self.user, 417 identifier=self.token_identifier, 418 intent=TokenIntents.INTENT_API, 419 description=f"Autogenerated by authentik for Outpost {self.name}", 420 expiring=False, 421 managed=managed, 422 ) 423 except IntegrityError: 424 # Integrity error happens mostly when managed is reused 425 Token.objects.filter(managed=managed).delete() 426 Token.objects.filter(identifier=self.token_identifier).delete() 427 return self.token
Get/create token for auto-generated user
429 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 430 """Get an iterator of all objects the user needs read access to""" 431 objects: list[models.Model | str] = [ 432 self, 433 "authentik_events.add_event", 434 ] 435 for provider in Provider.objects.filter(outpost=self).select_related().select_subclasses(): 436 if isinstance(provider, OutpostModel): 437 objects.extend(provider.get_required_objects()) 438 else: 439 objects.append(provider) 440 if self.managed: 441 for brand in Brand.objects.filter(web_certificate__isnull=False): 442 objects.append(brand) 443 objects.append(("authentik_crypto.view_certificatekeypair", brand.web_certificate)) 444 objects.append( 445 ("authentik_crypto.view_certificatekeypair_certificate", brand.web_certificate) 446 ) 447 objects.append( 448 ("authentik_crypto.view_certificatekeypair_key", brand.web_certificate) 449 ) 450 return objects
Get an iterator of all objects the user needs read access to
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
Accessor to the related objects manager on the one-to-many relation created by GenericRelation.
In the example::
class Post(Model):
comments = GenericRelation(Comment)
post.comments is a ReverseGenericManyToOneDescriptor instance.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Method descriptor with partial application of the given arguments and keywords.
Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.
Inherited Members
The requested object does not exist
The query returned multiple objects when only one was expected.
460@dataclass 461class OutpostState: 462 """Outpost instance state, last_seen and version""" 463 464 uid: str 465 last_seen: datetime | None = field(default=None) 466 version: str | None = field(default=None) 467 version_should: Version = field(default=OUR_VERSION) 468 build_hash: str = field(default="") 469 golang_version: str = field(default="") 470 openssl_enabled: bool = field(default=False) 471 openssl_version: str = field(default="") 472 fips_enabled: bool = field(default=False) 473 hostname: str = field(default="") 474 args: dict = field(default_factory=dict) 475 476 _outpost: Outpost | None = field(default=None) 477 478 @property 479 def version_outdated(self) -> bool: 480 """Check if outpost version matches our version""" 481 if not self.version: 482 return False 483 if self.build_hash != authentik_build_hash(): 484 return False 485 return parse(self.version) != OUR_VERSION 486 487 @staticmethod 488 def for_outpost(outpost: Outpost) -> list[OutpostState]: 489 """Get all states for an outpost""" 490 keys = cache.keys(f"{outpost.state_cache_prefix}/*") 491 if not keys: 492 return [] 493 states = [] 494 for key in keys: 495 instance_uid = key.replace(f"{outpost.state_cache_prefix}/", "") 496 states.append(OutpostState.for_instance_uid(outpost, instance_uid)) 497 return states 498 499 @staticmethod 500 def for_instance_uid(outpost: Outpost, uid: str) -> OutpostState: 501 """Get state for a single instance""" 502 key = f"{outpost.state_cache_prefix}/{uid}" 503 default_data = {"uid": uid} 504 data = cache.get(key, default_data) 505 if isinstance(data, str): 506 cache.delete(key) 507 data = default_data 508 state = from_dict(OutpostState, data) 509 510 state._outpost = outpost 511 return state 512 513 def save(self, timeout=OUTPOST_HELLO_INTERVAL): 514 """Save current state to cache""" 515 full_key = f"{self._outpost.state_cache_prefix}/{self.uid}" 516 return cache.set(full_key, asdict(self), timeout=timeout) 517 518 def delete(self): 519 """Manually delete from cache, used on channel disconnect""" 520 full_key = f"{self._outpost.state_cache_prefix}/{self.uid}" 521 cache.delete(full_key)
Outpost instance state, last_seen and version
478 @property 479 def version_outdated(self) -> bool: 480 """Check if outpost version matches our version""" 481 if not self.version: 482 return False 483 if self.build_hash != authentik_build_hash(): 484 return False 485 return parse(self.version) != OUR_VERSION
Check if outpost version matches our version
487 @staticmethod 488 def for_outpost(outpost: Outpost) -> list[OutpostState]: 489 """Get all states for an outpost""" 490 keys = cache.keys(f"{outpost.state_cache_prefix}/*") 491 if not keys: 492 return [] 493 states = [] 494 for key in keys: 495 instance_uid = key.replace(f"{outpost.state_cache_prefix}/", "") 496 states.append(OutpostState.for_instance_uid(outpost, instance_uid)) 497 return states
Get all states for an outpost
499 @staticmethod 500 def for_instance_uid(outpost: Outpost, uid: str) -> OutpostState: 501 """Get state for a single instance""" 502 key = f"{outpost.state_cache_prefix}/{uid}" 503 default_data = {"uid": uid} 504 data = cache.get(key, default_data) 505 if isinstance(data, str): 506 cache.delete(key) 507 data = default_data 508 state = from_dict(OutpostState, data) 509 510 state._outpost = outpost 511 return state
Get state for a single instance