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 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 kubernetes_disable_x509_strict: bool = field(default=False) 85 86 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 96 97 98class OutpostType(models.TextChoices): 99 """Outpost types""" 100 101 PROXY = "proxy" 102 LDAP = "ldap" 103 RADIUS = "radius" 104 RAC = "rac" 105 106 107def default_outpost_config(host: str | None = None): 108 """Get default outpost config""" 109 return asdict(OutpostConfig(authentik_host=host or "")) 110 111 112@dataclass 113class OutpostServiceConnectionState: 114 """State of an Outpost Service Connection""" 115 116 version: str 117 healthy: bool 118 119 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 ] 178 179 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" 229 230 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" 261 262 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") 458 459 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 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) 85 kubernetes_disable_x509_strict: bool = field(default=False)
Configuration an outpost uses to configure it self
88class OutpostModel(Model): 89 """Base model for providers that need more objects than just themselves""" 90 91 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 92 """Return a list of all required objects""" 93 return [self] 94 95 class Meta: 96 abstract = True
Base model for providers that need more objects than just themselves
91 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 92 """Return a list of all required objects""" 93 return [self]
Return a list of all required objects
99class OutpostType(models.TextChoices): 100 """Outpost types""" 101 102 PROXY = "proxy" 103 LDAP = "ldap" 104 RADIUS = "radius" 105 RAC = "rac"
Outpost types
108def default_outpost_config(host: str | None = None): 109 """Get default outpost config""" 110 return asdict(OutpostConfig(authentik_host=host or ""))
Get default outpost config
113@dataclass 114class OutpostServiceConnectionState: 115 """State of an Outpost Service Connection""" 116 117 version: str 118 healthy: bool
State of an Outpost Service Connection
121class OutpostServiceConnection(ScheduledModel, models.Model): 122 """Connection details for an Outpost Controller, like Docker or Kubernetes""" 123 124 uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) 125 name = models.TextField(unique=True) 126 127 local = models.BooleanField( 128 default=False, 129 help_text=_( 130 "If enabled, use the local connection. Required Docker socket/Kubernetes Integration" 131 ), 132 ) 133 134 objects = InheritanceManager() 135 136 class Meta: 137 verbose_name = _("Outpost Service-Connection") 138 verbose_name_plural = _("Outpost Service-Connections") 139 140 def __str__(self) -> str: 141 return f"Outpost service connection {self.name}" 142 143 @property 144 def state_key(self) -> str: 145 """Key used to save connection state in cache""" 146 return f"goauthentik.io/outposts/service_connection_state/{self.pk.hex}" 147 148 @property 149 def state(self) -> OutpostServiceConnectionState: 150 """Get state of service connection""" 151 from authentik.outposts.tasks import outpost_service_connection_monitor 152 153 state = cache.get(self.state_key, None) 154 if not state: 155 outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self) 156 return OutpostServiceConnectionState("", False) 157 return state 158 159 @property 160 def component(self) -> str: 161 """Return component used to edit this object""" 162 # This is called when creating an outpost with a service connection 163 # since the response doesn't use the correct inheritance 164 return "" 165 166 @property 167 def schedule_specs(self) -> list[ScheduleSpec]: 168 from authentik.outposts.tasks import outpost_service_connection_monitor 169 170 return [ 171 ScheduleSpec( 172 actor=outpost_service_connection_monitor, 173 uid=self.name, 174 args=(self.pk,), 175 crontab="3-59/15 * * * *", 176 send_on_save=True, 177 ), 178 ]
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.
143 @property 144 def state_key(self) -> str: 145 """Key used to save connection state in cache""" 146 return f"goauthentik.io/outposts/service_connection_state/{self.pk.hex}"
Key used to save connection state in cache
148 @property 149 def state(self) -> OutpostServiceConnectionState: 150 """Get state of service connection""" 151 from authentik.outposts.tasks import outpost_service_connection_monitor 152 153 state = cache.get(self.state_key, None) 154 if not state: 155 outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self) 156 return OutpostServiceConnectionState("", False) 157 return state
Get state of service connection
159 @property 160 def component(self) -> str: 161 """Return component used to edit this object""" 162 # This is called when creating an outpost with a service connection 163 # since the response doesn't use the correct inheritance 164 return ""
Return component used to edit this object
166 @property 167 def schedule_specs(self) -> list[ScheduleSpec]: 168 from authentik.outposts.tasks import outpost_service_connection_monitor 169 170 return [ 171 ScheduleSpec( 172 actor=outpost_service_connection_monitor, 173 uid=self.name, 174 args=(self.pk,), 175 crontab="3-59/15 * * * *", 176 send_on_save=True, 177 ), 178 ]
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.
181class DockerServiceConnection(SerializerModel, OutpostServiceConnection): 182 """Service Connection to a Docker endpoint""" 183 184 url = models.TextField( 185 help_text=_( 186 "Can be in the format of 'unix://<path>' when connecting to a local docker daemon, " 187 "or 'https://<hostname>:2376' when connecting to a remote system." 188 ) 189 ) 190 tls_verification = models.ForeignKey( 191 CertificateKeyPair, 192 null=True, 193 blank=True, 194 default=None, 195 related_name="+", 196 on_delete=models.SET_DEFAULT, 197 help_text=_( 198 "CA which the endpoint's Certificate is verified against. " 199 "Can be left empty for no validation." 200 ), 201 ) 202 tls_authentication = models.ForeignKey( 203 CertificateKeyPair, 204 null=True, 205 blank=True, 206 default=None, 207 related_name="+", 208 on_delete=models.SET_DEFAULT, 209 help_text=_( 210 "Certificate/Key used for authentication. Can be left empty for no authentication." 211 ), 212 ) 213 214 class Meta: 215 verbose_name = _("Docker Service-Connection") 216 verbose_name_plural = _("Docker Service-Connections") 217 218 def __str__(self) -> str: 219 return f"Docker Service-Connection {self.name}" 220 221 @property 222 def serializer(self) -> Serializer: 223 from authentik.outposts.api.service_connections import DockerServiceConnectionSerializer 224 225 return DockerServiceConnectionSerializer 226 227 @property 228 def component(self) -> str: 229 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.
221 @property 222 def serializer(self) -> Serializer: 223 from authentik.outposts.api.service_connections import DockerServiceConnectionSerializer 224 225 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.
232class KubernetesServiceConnection(SerializerModel, OutpostServiceConnection): 233 """Service Connection to a Kubernetes cluster""" 234 235 kubeconfig = models.JSONField( 236 help_text=_( 237 "Paste your kubeconfig here. authentik will automatically use " 238 "the currently selected context." 239 ), 240 blank=True, 241 ) 242 verify_ssl = models.BooleanField( 243 default=True, help_text=_("Verify SSL Certificates of the Kubernetes API endpoint") 244 ) 245 246 class Meta: 247 verbose_name = _("Kubernetes Service-Connection") 248 verbose_name_plural = _("Kubernetes Service-Connections") 249 250 def __str__(self) -> str: 251 return f"Kubernetes Service-Connection {self.name}" 252 253 @property 254 def serializer(self) -> Serializer: 255 from authentik.outposts.api.service_connections import KubernetesServiceConnectionSerializer 256 257 return KubernetesServiceConnectionSerializer 258 259 @property 260 def component(self) -> str: 261 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.
253 @property 254 def serializer(self) -> Serializer: 255 from authentik.outposts.api.service_connections import KubernetesServiceConnectionSerializer 256 257 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.
264class Outpost(ScheduledModel, SerializerModel, ManagedModel): 265 """Outpost instance which manages a service user and token""" 266 267 uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) 268 name = models.TextField(unique=True) 269 270 type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY) 271 service_connection = InheritanceForeignKey( 272 OutpostServiceConnection, 273 default=None, 274 null=True, 275 blank=True, 276 help_text=_( 277 "Select Service-Connection authentik should use to manage this outpost. " 278 "Leave empty if authentik should not handle the deployment." 279 ), 280 on_delete=models.SET_DEFAULT, 281 ) 282 283 _config = models.JSONField(default=default_outpost_config) 284 285 providers = models.ManyToManyField(Provider) 286 287 @property 288 def serializer(self) -> Serializer: 289 from authentik.outposts.api.outposts import OutpostSerializer 290 291 return OutpostSerializer 292 293 @property 294 def config(self) -> OutpostConfig: 295 """Load config as OutpostConfig object""" 296 return from_dict(OutpostConfig, self._config) 297 298 @config.setter 299 def config(self, value): 300 """Dump config into json""" 301 self._config = asdict(value) 302 303 @property 304 def state_cache_prefix(self) -> str: 305 """Key by which the outposts status is saved""" 306 return f"goauthentik.io/outposts/state/{self.uuid.hex}" 307 308 @property 309 def state(self) -> list[OutpostState]: 310 """Get outpost's health status""" 311 return OutpostState.for_outpost(self) 312 313 @property 314 def user_identifier(self): 315 """Username for service user""" 316 return f"ak-outpost-{self.uuid.hex}" 317 318 @property 319 def schedule_specs(self) -> list[ScheduleSpec]: 320 from authentik.outposts.tasks import outpost_controller 321 322 return [ 323 ScheduleSpec( 324 actor=outpost_controller, 325 uid=self.name, 326 args=(self.pk,), 327 kwargs={"action": "up", "from_cache": False}, 328 crontab=f"{fqdn_rand('outpost_controller')} */4 * * *", 329 send_on_save=True, 330 ), 331 ] 332 333 def build_user_permissions(self, user: User): 334 """Create per-object and global permissions for outpost service-account""" 335 # To ensure the user only has the correct permissions, we delete all of them and re-add 336 # the ones the user needs 337 try: 338 with transaction.atomic(): 339 user.remove_all_perms_from_managed_role() 340 for model_or_perm in self.get_required_objects(): 341 if isinstance(model_or_perm, models.Model): 342 code_name = ( 343 f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}" 344 ) 345 user.assign_perms_to_managed_role(code_name, model_or_perm) 346 elif isinstance(model_or_perm, tuple): 347 perm, obj = model_or_perm 348 user.assign_perms_to_managed_role(perm, obj) 349 else: 350 user.assign_perms_to_managed_role(model_or_perm) 351 except (Permission.DoesNotExist, AttributeError) as exc: 352 LOGGER.warning( 353 "permission doesn't exist", 354 code_name=code_name, 355 user=user, 356 model=model_or_perm, 357 ) 358 Event.new( 359 action=EventAction.SYSTEM_EXCEPTION, 360 message=( 361 "While setting the permissions for the service-account, a " 362 "permission was not found: Check " 363 "https://docs.goauthentik.io/troubleshooting/missing_permission" 364 ), 365 ).with_exception(exc).set_user(user).save() 366 LOGGER.debug( 367 "Updated service account's permissions", 368 obj_perms=user.get_all_obj_perms_on_managed_role(), 369 perms=user.get_all_model_perms_on_managed_role(), 370 ) 371 372 @property 373 def user(self) -> User: 374 """Get/create user with access to all required objects""" 375 user = User.objects.filter(username=self.user_identifier).first() 376 user_created = False 377 if not user: 378 user: User = User.objects.create(username=self.user_identifier) 379 user_created = True 380 attrs = { 381 "type": UserTypes.INTERNAL_SERVICE_ACCOUNT, 382 "name": f"Outpost {self.name} Service-Account", 383 "path": USER_PATH_OUTPOSTS, 384 } 385 dirty = False 386 for key, value in attrs.items(): 387 if getattr(user, key) != value: 388 dirty = True 389 setattr(user, key, value) 390 if user.has_usable_password(): 391 user.set_unusable_password() 392 dirty = True 393 if dirty: 394 user.save() 395 if user_created: 396 self.build_user_permissions(user) 397 return user 398 399 @property 400 def token_identifier(self) -> str: 401 """Get Token identifier""" 402 return f"ak-outpost-{self.pk}-api" 403 404 @property 405 def token(self) -> Token: 406 """Get/create token for auto-generated user""" 407 managed = f"goauthentik.io/outpost/{self.token_identifier}" 408 tokens = Token.objects.filter( 409 identifier=self.token_identifier, 410 intent=TokenIntents.INTENT_API, 411 managed=managed, 412 ) 413 if tokens.exists(): 414 return tokens.first() 415 try: 416 return Token.objects.create( 417 user=self.user, 418 identifier=self.token_identifier, 419 intent=TokenIntents.INTENT_API, 420 description=f"Autogenerated by authentik for Outpost {self.name}", 421 expiring=False, 422 managed=managed, 423 ) 424 except IntegrityError: 425 # Integrity error happens mostly when managed is reused 426 Token.objects.filter(managed=managed).delete() 427 Token.objects.filter(identifier=self.token_identifier).delete() 428 return self.token 429 430 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 431 """Get an iterator of all objects the user needs read access to""" 432 objects: list[models.Model | str] = [ 433 self, 434 "authentik_events.add_event", 435 ] 436 for provider in Provider.objects.filter(outpost=self).select_related().select_subclasses(): 437 if isinstance(provider, OutpostModel): 438 objects.extend(provider.get_required_objects()) 439 else: 440 objects.append(provider) 441 if self.managed: 442 for brand in Brand.objects.filter(web_certificate__isnull=False): 443 objects.append(brand) 444 objects.append(("authentik_crypto.view_certificatekeypair", brand.web_certificate)) 445 objects.append( 446 ("authentik_crypto.view_certificatekeypair_certificate", brand.web_certificate) 447 ) 448 objects.append( 449 ("authentik_crypto.view_certificatekeypair_key", brand.web_certificate) 450 ) 451 return objects 452 453 def __str__(self) -> str: 454 return f"Outpost {self.name}" 455 456 class Meta: 457 verbose_name = _("Outpost") 458 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.
287 @property 288 def serializer(self) -> Serializer: 289 from authentik.outposts.api.outposts import OutpostSerializer 290 291 return OutpostSerializer
Get serializer for this model
293 @property 294 def config(self) -> OutpostConfig: 295 """Load config as OutpostConfig object""" 296 return from_dict(OutpostConfig, self._config)
Load config as OutpostConfig object
303 @property 304 def state_cache_prefix(self) -> str: 305 """Key by which the outposts status is saved""" 306 return f"goauthentik.io/outposts/state/{self.uuid.hex}"
Key by which the outposts status is saved
308 @property 309 def state(self) -> list[OutpostState]: 310 """Get outpost's health status""" 311 return OutpostState.for_outpost(self)
Get outpost's health status
313 @property 314 def user_identifier(self): 315 """Username for service user""" 316 return f"ak-outpost-{self.uuid.hex}"
Username for service user
318 @property 319 def schedule_specs(self) -> list[ScheduleSpec]: 320 from authentik.outposts.tasks import outpost_controller 321 322 return [ 323 ScheduleSpec( 324 actor=outpost_controller, 325 uid=self.name, 326 args=(self.pk,), 327 kwargs={"action": "up", "from_cache": False}, 328 crontab=f"{fqdn_rand('outpost_controller')} */4 * * *", 329 send_on_save=True, 330 ), 331 ]
333 def build_user_permissions(self, user: User): 334 """Create per-object and global permissions for outpost service-account""" 335 # To ensure the user only has the correct permissions, we delete all of them and re-add 336 # the ones the user needs 337 try: 338 with transaction.atomic(): 339 user.remove_all_perms_from_managed_role() 340 for model_or_perm in self.get_required_objects(): 341 if isinstance(model_or_perm, models.Model): 342 code_name = ( 343 f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}" 344 ) 345 user.assign_perms_to_managed_role(code_name, model_or_perm) 346 elif isinstance(model_or_perm, tuple): 347 perm, obj = model_or_perm 348 user.assign_perms_to_managed_role(perm, obj) 349 else: 350 user.assign_perms_to_managed_role(model_or_perm) 351 except (Permission.DoesNotExist, AttributeError) as exc: 352 LOGGER.warning( 353 "permission doesn't exist", 354 code_name=code_name, 355 user=user, 356 model=model_or_perm, 357 ) 358 Event.new( 359 action=EventAction.SYSTEM_EXCEPTION, 360 message=( 361 "While setting the permissions for the service-account, a " 362 "permission was not found: Check " 363 "https://docs.goauthentik.io/troubleshooting/missing_permission" 364 ), 365 ).with_exception(exc).set_user(user).save() 366 LOGGER.debug( 367 "Updated service account's permissions", 368 obj_perms=user.get_all_obj_perms_on_managed_role(), 369 perms=user.get_all_model_perms_on_managed_role(), 370 )
Create per-object and global permissions for outpost service-account
372 @property 373 def user(self) -> User: 374 """Get/create user with access to all required objects""" 375 user = User.objects.filter(username=self.user_identifier).first() 376 user_created = False 377 if not user: 378 user: User = User.objects.create(username=self.user_identifier) 379 user_created = True 380 attrs = { 381 "type": UserTypes.INTERNAL_SERVICE_ACCOUNT, 382 "name": f"Outpost {self.name} Service-Account", 383 "path": USER_PATH_OUTPOSTS, 384 } 385 dirty = False 386 for key, value in attrs.items(): 387 if getattr(user, key) != value: 388 dirty = True 389 setattr(user, key, value) 390 if user.has_usable_password(): 391 user.set_unusable_password() 392 dirty = True 393 if dirty: 394 user.save() 395 if user_created: 396 self.build_user_permissions(user) 397 return user
Get/create user with access to all required objects
399 @property 400 def token_identifier(self) -> str: 401 """Get Token identifier""" 402 return f"ak-outpost-{self.pk}-api"
Get Token identifier
404 @property 405 def token(self) -> Token: 406 """Get/create token for auto-generated user""" 407 managed = f"goauthentik.io/outpost/{self.token_identifier}" 408 tokens = Token.objects.filter( 409 identifier=self.token_identifier, 410 intent=TokenIntents.INTENT_API, 411 managed=managed, 412 ) 413 if tokens.exists(): 414 return tokens.first() 415 try: 416 return Token.objects.create( 417 user=self.user, 418 identifier=self.token_identifier, 419 intent=TokenIntents.INTENT_API, 420 description=f"Autogenerated by authentik for Outpost {self.name}", 421 expiring=False, 422 managed=managed, 423 ) 424 except IntegrityError: 425 # Integrity error happens mostly when managed is reused 426 Token.objects.filter(managed=managed).delete() 427 Token.objects.filter(identifier=self.token_identifier).delete() 428 return self.token
Get/create token for auto-generated user
430 def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]: 431 """Get an iterator of all objects the user needs read access to""" 432 objects: list[models.Model | str] = [ 433 self, 434 "authentik_events.add_event", 435 ] 436 for provider in Provider.objects.filter(outpost=self).select_related().select_subclasses(): 437 if isinstance(provider, OutpostModel): 438 objects.extend(provider.get_required_objects()) 439 else: 440 objects.append(provider) 441 if self.managed: 442 for brand in Brand.objects.filter(web_certificate__isnull=False): 443 objects.append(brand) 444 objects.append(("authentik_crypto.view_certificatekeypair", brand.web_certificate)) 445 objects.append( 446 ("authentik_crypto.view_certificatekeypair_certificate", brand.web_certificate) 447 ) 448 objects.append( 449 ("authentik_crypto.view_certificatekeypair_key", brand.web_certificate) 450 ) 451 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.
461@dataclass 462class OutpostState: 463 """Outpost instance state, last_seen and version""" 464 465 uid: str 466 last_seen: datetime | None = field(default=None) 467 version: str | None = field(default=None) 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