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)
OUR_VERSION = <Version('2026.5.0rc1')>
OUTPOST_HELLO_INTERVAL = 10
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
USER_PATH_OUTPOSTS = 'goauthentik.io/outposts'
class ServiceConnectionInvalid(authentik.lib.sentry.SentryIgnoredException):
49class ServiceConnectionInvalid(SentryIgnoredException):
50    """Exception raised when a Service Connection has invalid parameters"""

Exception raised when a Service Connection has invalid parameters

@dataclass
class OutpostConfig:
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

OutpostConfig( authentik_host: str = '', authentik_host_insecure: bool = False, authentik_host_browser: str = '', log_level: str = 'debug', object_naming_template: str = 'ak-outpost-%(name)s', refresh_interval: str = 'minutes=5', container_image: str | None = None, docker_network: str | None = None, docker_map_ports: bool = True, docker_labels: dict[str, str] | None = None, kubernetes_replicas: int = 1, kubernetes_namespace: str = <factory>, kubernetes_ingress_annotations: dict[str, str] = <factory>, kubernetes_ingress_secret_name: str = 'authentik-outpost-tls', kubernetes_ingress_class_name: str | None = None, kubernetes_ingress_path_type: str | None = None, kubernetes_httproute_annotations: dict[str, str] = <factory>, kubernetes_httproute_parent_refs: list[dict[str, str]] = <factory>, kubernetes_service_type: str = 'ClusterIP', kubernetes_disabled_components: list[str] = <factory>, kubernetes_image_pull_secrets: list[str] = <factory>, kubernetes_json_patches: dict[str, list[dict[str, Any]]] | None = None)
authentik_host: str = ''
authentik_host_insecure: bool = False
authentik_host_browser: str = ''
log_level: str = 'debug'
object_naming_template: str = 'ak-outpost-%(name)s'
refresh_interval: str = 'minutes=5'
container_image: str | None = None
docker_network: str | None = None
docker_map_ports: bool = True
docker_labels: dict[str, str] | None = None
kubernetes_replicas: int = 1
kubernetes_namespace: str
kubernetes_ingress_annotations: dict[str, str]
kubernetes_ingress_secret_name: str = 'authentik-outpost-tls'
kubernetes_ingress_class_name: str | None = None
kubernetes_ingress_path_type: str | None = None
kubernetes_httproute_annotations: dict[str, str]
kubernetes_httproute_parent_refs: list[dict[str, str]]
kubernetes_service_type: str = 'ClusterIP'
kubernetes_disabled_components: list[str]
kubernetes_image_pull_secrets: list[str]
kubernetes_json_patches: dict[str, list[dict[str, Any]]] | None = None
class OutpostModel(django.db.models.base.Model):
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

def get_required_objects( self) -> Iterable[django.db.models.base.Model | str | tuple[str, django.db.models.base.Model]]:
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

class OutpostModel.Meta:
94    class Meta:
95        abstract = True
abstract = False
class OutpostType(django.db.models.enums.TextChoices):
 98class OutpostType(models.TextChoices):
 99    """Outpost types"""
100
101    PROXY = "proxy"
102    LDAP = "ldap"
103    RADIUS = "radius"
104    RAC = "rac"

Outpost types

def default_outpost_config(host: str | None = None):
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

@dataclass
class OutpostServiceConnectionState:
112@dataclass
113class OutpostServiceConnectionState:
114    """State of an Outpost Service Connection"""
115
116    version: str
117    healthy: bool

State of an Outpost Service Connection

OutpostServiceConnectionState(version: str, healthy: bool)
version: str
healthy: bool
class OutpostServiceConnection(authentik.tasks.schedules.models.ScheduledModel, django.db.models.base.Model):
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

def uuid(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def name(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def local(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def objects(unknown):

The type of the None singleton.

state_key: str
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

state: OutpostServiceConnectionState
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

component: str
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

schedule_specs: list[authentik.tasks.schedules.common.ScheduleSpec]
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        ]
schedules

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.

tasks

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.

dockerserviceconnection

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.

kubernetesserviceconnection

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.

outpost_set

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.

class OutpostServiceConnection.DoesNotExist(django.core.exceptions.ObjectDoesNotExist):

The requested object does not exist

class OutpostServiceConnection.MultipleObjectsReturned(django.core.exceptions.MultipleObjectsReturned):

The query returned multiple objects when only one was expected.

class DockerServiceConnection(authentik.lib.models.SerializerModel, OutpostServiceConnection):
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

def url(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

tls_verification

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.

tls_authentication

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.

serializer: rest_framework.serializers.Serializer
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

component: str
226    @property
227    def component(self) -> str:
228        return "ak-service-connection-docker-form"

Return component used to edit this object

schedules

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.

tasks

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.

tls_verification_id
tls_authentication_id
outpostserviceconnection_ptr_id
outpostserviceconnection_ptr

Accessor to the related object on the forward side of a one-to-one relation.

In the example::

class Restaurant(Model):
    place = OneToOneField(Place, related_name='restaurant')

Restaurant.place is a ForwardOneToOneDescriptor instance.

class DockerServiceConnection.DoesNotExist(OutpostServiceConnection.DoesNotExist):

The requested object does not exist

class DockerServiceConnection.MultipleObjectsReturned(OutpostServiceConnection.MultipleObjectsReturned):

The query returned multiple objects when only one was expected.

class KubernetesServiceConnection(authentik.lib.models.SerializerModel, OutpostServiceConnection):
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

def kubeconfig(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def verify_ssl(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

serializer: rest_framework.serializers.Serializer
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

component: str
258    @property
259    def component(self) -> str:
260        return "ak-service-connection-kubernetes-form"

Return component used to edit this object

schedules

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.

tasks

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.

outpostserviceconnection_ptr_id
outpostserviceconnection_ptr

Accessor to the related object on the forward side of a one-to-one relation.

In the example::

class Restaurant(Model):
    place = OneToOneField(Place, related_name='restaurant')

Restaurant.place is a ForwardOneToOneDescriptor instance.

class KubernetesServiceConnection.DoesNotExist(OutpostServiceConnection.DoesNotExist):

The requested object does not exist

class KubernetesServiceConnection.MultipleObjectsReturned(OutpostServiceConnection.MultipleObjectsReturned):

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

def uuid(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def name(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def type(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

service_connection

Forward ManyToOne Descriptor that selects subclass. Requires InheritanceAutoManager.

providers

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.

serializer: rest_framework.serializers.Serializer
286    @property
287    def serializer(self) -> Serializer:
288        from authentik.outposts.api.outposts import OutpostSerializer
289
290        return OutpostSerializer

Get serializer for this model

config: OutpostConfig
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

state_cache_prefix: str
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

state: list[OutpostState]
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

user_identifier
312    @property
313    def user_identifier(self):
314        """Username for service user"""
315        return f"ak-outpost-{self.uuid.hex}"

Username for service user

schedule_specs: list[authentik.tasks.schedules.common.ScheduleSpec]
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        ]
def build_user_permissions(self, user: authentik.core.models.User):
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

user: authentik.core.models.User
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

token_identifier: str
398    @property
399    def token_identifier(self) -> str:
400        """Get Token identifier"""
401        return f"ak-outpost-{self.pk}-api"

Get Token identifier

token: authentik.core.models.Token
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

def get_required_objects( self) -> Iterable[django.db.models.base.Model | str | tuple[str, django.db.models.base.Model]]:
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

schedules

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.

tasks

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.

def managed(unknown):

A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.

def get_type_display(unknown):

Method descriptor with partial application of the given arguments and keywords.

Supports wrapping existing descriptors and handles non-descriptor callables as instance methods.

service_connection_id
def objects(unknown):

The type of the None singleton.

class Outpost.DoesNotExist(django.core.exceptions.ObjectDoesNotExist):

The requested object does not exist

class Outpost.MultipleObjectsReturned(django.core.exceptions.MultipleObjectsReturned):

The query returned multiple objects when only one was expected.

@dataclass
class OutpostState:
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

OutpostState( uid: str, last_seen: datetime.datetime | None = None, version: str | None = None, version_should: packaging.version.Version = <Version('2026.5.0rc1')>, build_hash: str = '', golang_version: str = '', openssl_enabled: bool = False, openssl_version: str = '', fips_enabled: bool = False, hostname: str = '', args: dict = <factory>, _outpost: Outpost | None = None)
uid: str
last_seen: datetime.datetime | None = None
version: str | None = None
version_should: packaging.version.Version = <Version('2026.5.0rc1')>
build_hash: str = ''
golang_version: str = ''
openssl_enabled: bool = False
openssl_version: str = ''
fips_enabled: bool = False
hostname: str = ''
args: dict
version_outdated: bool
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

@staticmethod
def for_outpost( outpost: Outpost) -> list[OutpostState]:
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

@staticmethod
def for_instance_uid( outpost: Outpost, uid: str) -> OutpostState:
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

def save(self, timeout=10):
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)

Save current state to cache

def delete(self):
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)

Manually delete from cache, used on channel disconnect