2020-09-02 22:04:12 +00:00
|
|
|
"""Outpost models"""
|
2020-10-14 08:44:17 +00:00
|
|
|
from dataclasses import asdict, dataclass, field
|
2020-09-02 22:04:12 +00:00
|
|
|
from datetime import datetime
|
2023-07-22 00:29:28 +00:00
|
|
|
from typing import Any, Iterable, Optional
|
2020-09-02 22:04:12 +00:00
|
|
|
from uuid import uuid4
|
|
|
|
|
2022-09-06 22:23:25 +00:00
|
|
|
from dacite.core import from_dict
|
2021-04-26 21:28:26 +00:00
|
|
|
from django.contrib.auth.models import Permission
|
2020-09-02 22:04:12 +00:00
|
|
|
from django.core.cache import cache
|
2021-06-13 11:36:54 +00:00
|
|
|
from django.db import IntegrityError, models, transaction
|
2020-09-13 12:29:40 +00:00
|
|
|
from django.db.models.base import Model
|
2020-09-02 22:04:12 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2020-09-14 15:34:07 +00:00
|
|
|
from guardian.models import UserObjectPermission
|
2020-09-02 22:04:12 +00:00
|
|
|
from guardian.shortcuts import assign_perm
|
2020-11-04 09:41:18 +00:00
|
|
|
from model_utils.managers import InheritanceManager
|
2022-12-30 11:07:25 +00:00
|
|
|
from packaging.version import Version, parse
|
2022-07-31 15:11:44 +00:00
|
|
|
from rest_framework.serializers import Serializer
|
2021-01-01 14:39:43 +00:00
|
|
|
from structlog.stdlib import get_logger
|
2020-09-02 22:04:12 +00:00
|
|
|
|
2022-01-13 16:47:31 +00:00
|
|
|
from authentik import __version__, get_build_hash
|
2022-07-31 15:11:44 +00:00
|
|
|
from authentik.blueprints.models import ManagedModel
|
2023-11-06 11:46:14 +00:00
|
|
|
from authentik.brands.models import Brand
|
2021-07-19 11:17:13 +00:00
|
|
|
from authentik.core.models import (
|
2022-06-15 10:12:26 +00:00
|
|
|
USER_PATH_SYSTEM_PREFIX,
|
2021-07-19 11:17:13 +00:00
|
|
|
Provider,
|
|
|
|
Token,
|
|
|
|
TokenIntents,
|
|
|
|
User,
|
2023-07-17 15:57:08 +00:00
|
|
|
UserTypes,
|
2021-07-19 11:17:13 +00:00
|
|
|
)
|
2020-12-05 21:08:42 +00:00
|
|
|
from authentik.crypto.models import CertificateKeyPair
|
2021-08-23 12:39:47 +00:00
|
|
|
from authentik.events.models import Event, EventAction
|
2020-12-05 21:08:42 +00:00
|
|
|
from authentik.lib.config import CONFIG
|
2022-07-31 15:11:44 +00:00
|
|
|
from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
2020-12-05 21:08:42 +00:00
|
|
|
from authentik.lib.sentry import SentryIgnoredException
|
2021-08-23 12:39:47 +00:00
|
|
|
from authentik.lib.utils.errors import exception_to_string
|
2021-05-09 16:44:32 +00:00
|
|
|
from authentik.outposts.controllers.k8s.utils import get_namespace
|
2020-09-02 22:04:12 +00:00
|
|
|
|
2020-09-18 22:54:48 +00:00
|
|
|
OUR_VERSION = parse(__version__)
|
2020-10-14 08:44:17 +00:00
|
|
|
OUTPOST_HELLO_INTERVAL = 10
|
2020-11-18 23:53:33 +00:00
|
|
|
LOGGER = get_logger()
|
2020-09-18 22:54:48 +00:00
|
|
|
|
2022-06-15 10:12:26 +00:00
|
|
|
USER_PATH_OUTPOSTS = USER_PATH_SYSTEM_PREFIX + "/outposts"
|
|
|
|
|
2020-09-02 22:04:12 +00:00
|
|
|
|
2020-11-08 20:02:52 +00:00
|
|
|
class ServiceConnectionInvalid(SentryIgnoredException):
|
2021-05-11 22:42:46 +00:00
|
|
|
"""Exception raised when a Service Connection has invalid parameters"""
|
2020-11-08 20:02:52 +00:00
|
|
|
|
|
|
|
|
2020-09-02 22:04:12 +00:00
|
|
|
@dataclass
|
2021-08-27 17:10:30 +00:00
|
|
|
# pylint: disable=too-many-instance-attributes
|
2020-09-02 22:04:12 +00:00
|
|
|
class OutpostConfig:
|
|
|
|
"""Configuration an outpost uses to configure it self"""
|
|
|
|
|
2022-11-14 13:24:11 +00:00
|
|
|
# update website/docs/outposts/_config.md
|
2021-06-13 21:56:38 +00:00
|
|
|
|
2021-08-08 14:57:56 +00:00
|
|
|
authentik_host: str = ""
|
2020-12-05 21:08:42 +00:00
|
|
|
authentik_host_insecure: bool = False
|
2021-09-26 10:00:51 +00:00
|
|
|
authentik_host_browser: str = ""
|
2020-09-02 22:04:12 +00:00
|
|
|
|
2023-07-19 21:13:22 +00:00
|
|
|
log_level: str = CONFIG.get("log_level")
|
2021-05-05 13:35:56 +00:00
|
|
|
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
2021-08-27 17:10:30 +00:00
|
|
|
|
2022-11-14 13:24:11 +00:00
|
|
|
container_image: Optional[str] = field(default=None)
|
|
|
|
|
2021-08-27 17:10:30 +00:00
|
|
|
docker_network: Optional[str] = field(default=None)
|
2021-09-29 21:55:22 +00:00
|
|
|
docker_map_ports: bool = field(default=True)
|
2022-01-23 20:55:58 +00:00
|
|
|
docker_labels: Optional[dict[str, str]] = field(default=None)
|
2021-08-27 17:10:30 +00:00
|
|
|
|
2020-10-14 15:49:09 +00:00
|
|
|
kubernetes_replicas: int = field(default=1)
|
2021-05-09 16:44:32 +00:00
|
|
|
kubernetes_namespace: str = field(default_factory=get_namespace)
|
2021-02-18 12:41:03 +00:00
|
|
|
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
2021-05-08 14:11:38 +00:00
|
|
|
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
|
2022-11-14 13:24:11 +00:00
|
|
|
kubernetes_ingress_class_name: Optional[str] = field(default=None)
|
2021-05-05 13:37:56 +00:00
|
|
|
kubernetes_service_type: str = field(default="ClusterIP")
|
2021-05-10 17:27:48 +00:00
|
|
|
kubernetes_disabled_components: list[str] = field(default_factory=list)
|
2022-09-06 22:23:25 +00:00
|
|
|
kubernetes_image_pull_secrets: list[str] = field(default_factory=list)
|
2023-07-22 00:29:28 +00:00
|
|
|
kubernetes_json_patches: Optional[dict[str, list[dict[str, Any]]]] = field(default=None)
|
2020-10-14 15:49:09 +00:00
|
|
|
|
2020-09-02 22:04:12 +00:00
|
|
|
|
2020-09-13 12:29:40 +00:00
|
|
|
class OutpostModel(Model):
|
2020-09-02 22:04:12 +00:00
|
|
|
"""Base model for providers that need more objects than just themselves"""
|
|
|
|
|
2021-12-30 13:59:01 +00:00
|
|
|
def get_required_objects(self) -> Iterable[models.Model | str]:
|
2020-09-02 22:04:12 +00:00
|
|
|
"""Return a list of all required objects"""
|
|
|
|
return [self]
|
|
|
|
|
2020-09-13 12:29:40 +00:00
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
2020-09-02 22:04:12 +00:00
|
|
|
|
|
|
|
class OutpostType(models.TextChoices):
|
2023-12-30 20:33:14 +00:00
|
|
|
"""Outpost types"""
|
2020-09-02 22:04:12 +00:00
|
|
|
|
|
|
|
PROXY = "proxy"
|
2021-04-19 22:30:27 +00:00
|
|
|
LDAP = "ldap"
|
2023-03-20 15:54:35 +00:00
|
|
|
RADIUS = "radius"
|
2023-12-30 20:33:14 +00:00
|
|
|
RAC = "rac"
|
2020-09-02 22:04:12 +00:00
|
|
|
|
|
|
|
|
2021-03-29 20:52:08 +00:00
|
|
|
def default_outpost_config(host: Optional[str] = None):
|
2020-09-02 22:04:12 +00:00
|
|
|
"""Get default outpost config"""
|
2021-03-29 20:52:08 +00:00
|
|
|
return asdict(OutpostConfig(authentik_host=host or ""))
|
2020-09-02 22:04:12 +00:00
|
|
|
|
|
|
|
|
2020-11-08 20:02:52 +00:00
|
|
|
@dataclass
|
|
|
|
class OutpostServiceConnectionState:
|
|
|
|
"""State of an Outpost Service Connection"""
|
|
|
|
|
|
|
|
version: str
|
|
|
|
healthy: bool
|
|
|
|
|
|
|
|
|
2020-11-04 09:41:18 +00:00
|
|
|
class OutpostServiceConnection(models.Model):
|
|
|
|
"""Connection details for an Outpost Controller, like Docker or Kubernetes"""
|
|
|
|
|
|
|
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
2023-03-07 22:52:34 +00:00
|
|
|
name = models.TextField(unique=True)
|
2020-11-04 09:41:18 +00:00
|
|
|
|
|
|
|
local = models.BooleanField(
|
|
|
|
default=False,
|
|
|
|
help_text=_(
|
2023-02-01 10:31:32 +00:00
|
|
|
"If enabled, use the local connection. Required Docker socket/Kubernetes Integration"
|
2020-11-04 09:41:18 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
objects = InheritanceManager()
|
|
|
|
|
2020-12-15 22:39:52 +00:00
|
|
|
@property
|
|
|
|
def state_key(self) -> str:
|
|
|
|
"""Key used to save connection state in cache"""
|
2023-04-28 10:53:07 +00:00
|
|
|
return f"goauthentik.io/outposts/service_connection_state/{self.pk.hex}"
|
2020-12-15 22:39:52 +00:00
|
|
|
|
2020-11-08 20:02:52 +00:00
|
|
|
@property
|
|
|
|
def state(self) -> OutpostServiceConnectionState:
|
|
|
|
"""Get state of service connection"""
|
2020-12-15 23:00:36 +00:00
|
|
|
from authentik.outposts.tasks import outpost_service_connection_state
|
|
|
|
|
2020-12-15 22:39:52 +00:00
|
|
|
state = cache.get(self.state_key, None)
|
2020-11-08 20:31:27 +00:00
|
|
|
if not state:
|
2020-12-15 22:39:52 +00:00
|
|
|
outpost_service_connection_state.delay(self.pk)
|
|
|
|
return OutpostServiceConnectionState("", False)
|
2020-11-08 20:02:52 +00:00
|
|
|
return state
|
|
|
|
|
2020-11-04 12:01:38 +00:00
|
|
|
@property
|
2021-03-31 20:40:48 +00:00
|
|
|
def component(self) -> str:
|
|
|
|
"""Return component used to edit this object"""
|
2021-06-15 16:39:51 +00:00
|
|
|
# This is called when creating an outpost with a service connection
|
|
|
|
# since the response doesn't use the correct inheritance
|
|
|
|
return ""
|
2020-11-04 12:01:38 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("Outpost Service-Connection")
|
|
|
|
verbose_name_plural = _("Outpost Service-Connections")
|
|
|
|
|
2020-11-04 09:41:18 +00:00
|
|
|
|
2022-07-31 15:11:44 +00:00
|
|
|
class DockerServiceConnection(SerializerModel, OutpostServiceConnection):
|
2020-11-04 12:01:38 +00:00
|
|
|
"""Service Connection to a Docker endpoint"""
|
2020-11-04 09:41:18 +00:00
|
|
|
|
2020-12-13 20:28:13 +00:00
|
|
|
url = models.TextField(
|
|
|
|
help_text=_(
|
2023-02-01 10:31:32 +00:00
|
|
|
"Can be in the format of 'unix://<path>' when connecting to a local docker daemon, "
|
|
|
|
"or 'https://<hostname>:2376' when connecting to a remote system."
|
2020-12-13 20:28:13 +00:00
|
|
|
)
|
|
|
|
)
|
2020-11-18 23:53:33 +00:00
|
|
|
tls_verification = models.ForeignKey(
|
|
|
|
CertificateKeyPair,
|
|
|
|
null=True,
|
|
|
|
blank=True,
|
|
|
|
default=None,
|
|
|
|
related_name="+",
|
|
|
|
on_delete=models.SET_DEFAULT,
|
|
|
|
help_text=_(
|
2023-02-01 10:31:32 +00:00
|
|
|
"CA which the endpoint's Certificate is verified against. "
|
|
|
|
"Can be left empty for no validation."
|
2020-11-18 23:53:33 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
tls_authentication = models.ForeignKey(
|
|
|
|
CertificateKeyPair,
|
|
|
|
null=True,
|
|
|
|
blank=True,
|
|
|
|
default=None,
|
|
|
|
related_name="+",
|
|
|
|
on_delete=models.SET_DEFAULT,
|
|
|
|
help_text=_(
|
|
|
|
"Certificate/Key used for authentication. Can be left empty for no authentication."
|
|
|
|
),
|
|
|
|
)
|
2020-11-04 09:41:18 +00:00
|
|
|
|
2022-07-31 15:11:44 +00:00
|
|
|
@property
|
|
|
|
def serializer(self) -> Serializer:
|
|
|
|
from authentik.outposts.api.service_connections import DockerServiceConnectionSerializer
|
|
|
|
|
|
|
|
return DockerServiceConnectionSerializer
|
|
|
|
|
2020-11-04 12:01:38 +00:00
|
|
|
@property
|
2021-03-31 20:40:48 +00:00
|
|
|
def component(self) -> str:
|
|
|
|
return "ak-service-connection-docker-form"
|
2020-11-04 12:01:38 +00:00
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
return f"Docker Service-Connection {self.name}"
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("Docker Service-Connection")
|
|
|
|
verbose_name_plural = _("Docker Service-Connections")
|
|
|
|
|
2020-11-04 09:41:18 +00:00
|
|
|
|
2022-07-31 15:11:44 +00:00
|
|
|
class KubernetesServiceConnection(SerializerModel, OutpostServiceConnection):
|
2020-11-04 12:01:38 +00:00
|
|
|
"""Service Connection to a Kubernetes cluster"""
|
|
|
|
|
|
|
|
kubeconfig = models.JSONField(
|
|
|
|
help_text=_(
|
2023-02-01 10:31:32 +00:00
|
|
|
"Paste your kubeconfig here. authentik will automatically use "
|
|
|
|
"the currently selected context."
|
2020-12-24 12:14:20 +00:00
|
|
|
),
|
|
|
|
blank=True,
|
2020-11-04 12:01:38 +00:00
|
|
|
)
|
2022-11-14 13:24:11 +00:00
|
|
|
verify_ssl = models.BooleanField(
|
|
|
|
default=True, help_text=_("Verify SSL Certificates of the Kubernetes API endpoint")
|
|
|
|
)
|
2020-11-04 12:01:38 +00:00
|
|
|
|
2022-07-31 15:11:44 +00:00
|
|
|
@property
|
|
|
|
def serializer(self) -> Serializer:
|
|
|
|
from authentik.outposts.api.service_connections import KubernetesServiceConnectionSerializer
|
|
|
|
|
|
|
|
return KubernetesServiceConnectionSerializer
|
|
|
|
|
2020-11-04 12:01:38 +00:00
|
|
|
@property
|
2021-03-31 20:40:48 +00:00
|
|
|
def component(self) -> str:
|
|
|
|
return "ak-service-connection-kubernetes-form"
|
2020-11-04 12:01:38 +00:00
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
return f"Kubernetes Service-Connection {self.name}"
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("Kubernetes Service-Connection")
|
|
|
|
verbose_name_plural = _("Kubernetes Service-Connections")
|
2020-11-04 09:41:18 +00:00
|
|
|
|
|
|
|
|
2022-07-31 15:11:44 +00:00
|
|
|
class Outpost(SerializerModel, ManagedModel):
|
2020-09-02 22:04:12 +00:00
|
|
|
"""Outpost instance which manages a service user and token"""
|
|
|
|
|
|
|
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
2023-03-07 22:52:34 +00:00
|
|
|
name = models.TextField(unique=True)
|
2020-09-02 22:04:12 +00:00
|
|
|
|
|
|
|
type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY)
|
2020-11-04 09:54:44 +00:00
|
|
|
service_connection = InheritanceForeignKey(
|
2020-11-04 09:41:18 +00:00
|
|
|
OutpostServiceConnection,
|
|
|
|
default=None,
|
|
|
|
null=True,
|
|
|
|
blank=True,
|
2020-09-02 22:04:12 +00:00
|
|
|
help_text=_(
|
2023-02-01 10:31:32 +00:00
|
|
|
"Select Service-Connection authentik should use to manage this outpost. "
|
|
|
|
"Leave empty if authentik should not handle the deployment."
|
2020-09-02 22:04:12 +00:00
|
|
|
),
|
2020-11-04 09:41:18 +00:00
|
|
|
on_delete=models.SET_DEFAULT,
|
2020-09-02 22:04:12 +00:00
|
|
|
)
|
2020-11-04 09:41:18 +00:00
|
|
|
|
2020-09-02 22:04:12 +00:00
|
|
|
_config = models.JSONField(default=default_outpost_config)
|
|
|
|
|
|
|
|
providers = models.ManyToManyField(Provider)
|
|
|
|
|
2022-07-31 15:11:44 +00:00
|
|
|
@property
|
|
|
|
def serializer(self) -> Serializer:
|
|
|
|
from authentik.outposts.api.outposts import OutpostSerializer
|
|
|
|
|
|
|
|
return OutpostSerializer
|
|
|
|
|
2020-09-02 22:04:12 +00:00
|
|
|
@property
|
|
|
|
def config(self) -> OutpostConfig:
|
|
|
|
"""Load config as OutpostConfig object"""
|
2020-09-13 20:19:26 +00:00
|
|
|
return from_dict(OutpostConfig, self._config)
|
2020-09-02 22:04:12 +00:00
|
|
|
|
|
|
|
@config.setter
|
|
|
|
def config(self, value):
|
|
|
|
"""Dump config into json"""
|
2020-09-13 20:19:26 +00:00
|
|
|
self._config = asdict(value)
|
2020-09-02 22:04:12 +00:00
|
|
|
|
2020-10-14 08:44:17 +00:00
|
|
|
@property
|
|
|
|
def state_cache_prefix(self) -> str:
|
2020-09-18 22:54:48 +00:00
|
|
|
"""Key by which the outposts status is saved"""
|
2023-04-28 10:53:07 +00:00
|
|
|
return f"goauthentik.io/outposts/state/{self.uuid.hex}"
|
2020-09-02 22:04:12 +00:00
|
|
|
|
|
|
|
@property
|
2021-02-18 12:45:46 +00:00
|
|
|
def state(self) -> list["OutpostState"]:
|
2020-09-02 22:04:12 +00:00
|
|
|
"""Get outpost's health status"""
|
2020-10-14 08:44:17 +00:00
|
|
|
return OutpostState.for_outpost(self)
|
2020-09-18 22:54:48 +00:00
|
|
|
|
2020-11-04 09:41:18 +00:00
|
|
|
@property
|
|
|
|
def user_identifier(self):
|
|
|
|
"""Username for service user"""
|
2020-12-05 21:08:42 +00:00
|
|
|
return f"ak-outpost-{self.uuid.hex}"
|
2020-11-04 09:41:18 +00:00
|
|
|
|
2021-10-04 16:04:19 +00:00
|
|
|
def build_user_permissions(self, user: User):
|
|
|
|
"""Create per-object and global permissions for outpost service-account"""
|
2020-09-14 15:34:07 +00:00
|
|
|
# To ensure the user only has the correct permissions, we delete all of them and re-add
|
|
|
|
# the ones the user needs
|
|
|
|
with transaction.atomic():
|
|
|
|
UserObjectPermission.objects.filter(user=user).delete()
|
2021-05-04 23:02:47 +00:00
|
|
|
user.user_permissions.clear()
|
2021-04-26 21:28:26 +00:00
|
|
|
for model_or_perm in self.get_required_objects():
|
|
|
|
if isinstance(model_or_perm, models.Model):
|
|
|
|
model_or_perm: models.Model
|
|
|
|
code_name = (
|
2023-02-01 10:31:32 +00:00
|
|
|
f"{model_or_perm._meta.app_label}.view_{model_or_perm._meta.model_name}"
|
2021-04-26 21:28:26 +00:00
|
|
|
)
|
2021-08-23 12:39:47 +00:00
|
|
|
try:
|
|
|
|
assign_perm(code_name, user, model_or_perm)
|
2021-08-28 12:51:04 +00:00
|
|
|
except (Permission.DoesNotExist, AttributeError) as exc:
|
2021-08-23 12:39:47 +00:00
|
|
|
LOGGER.warning(
|
|
|
|
"permission doesn't exist",
|
|
|
|
code_name=code_name,
|
|
|
|
user=user,
|
|
|
|
model=model_or_perm,
|
|
|
|
)
|
|
|
|
Event.new(
|
|
|
|
action=EventAction.SYSTEM_EXCEPTION,
|
2021-08-23 12:53:53 +00:00
|
|
|
message=(
|
2021-08-23 12:55:45 +00:00
|
|
|
"While setting the permissions for the service-account, a "
|
|
|
|
"permission was not found: Check "
|
|
|
|
"https://goauthentik.io/docs/troubleshooting/missing_permission"
|
|
|
|
)
|
|
|
|
+ exception_to_string(exc),
|
2021-08-23 12:39:47 +00:00
|
|
|
).set_user(user).save()
|
2021-04-26 21:28:26 +00:00
|
|
|
else:
|
2021-05-04 23:02:47 +00:00
|
|
|
app_label, perm = model_or_perm.split(".")
|
|
|
|
permission = Permission.objects.filter(
|
|
|
|
codename=perm,
|
|
|
|
content_type__app_label=app_label,
|
|
|
|
)
|
|
|
|
if not permission.exists():
|
|
|
|
LOGGER.warning("permission doesn't exist", perm=model_or_perm)
|
|
|
|
continue
|
|
|
|
user.user_permissions.add(permission.first())
|
2021-05-07 09:56:44 +00:00
|
|
|
LOGGER.debug(
|
|
|
|
"Updated service account's permissions",
|
2021-12-22 10:43:45 +00:00
|
|
|
obj_perms=UserObjectPermission.objects.filter(user=user),
|
|
|
|
perms=user.user_permissions.all(),
|
2021-05-07 09:56:44 +00:00
|
|
|
)
|
2021-10-04 16:04:19 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def user(self) -> User:
|
|
|
|
"""Get/create user with access to all required objects"""
|
2022-07-05 20:26:56 +00:00
|
|
|
user = User.objects.filter(username=self.user_identifier).first()
|
|
|
|
user_created = False
|
|
|
|
if not user:
|
2021-10-04 16:04:19 +00:00
|
|
|
user: User = User.objects.create(username=self.user_identifier)
|
2022-07-05 20:26:56 +00:00
|
|
|
user_created = True
|
2023-11-18 00:46:16 +00:00
|
|
|
attrs = {
|
|
|
|
"type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
|
|
|
"name": f"Outpost {self.name} Service-Account",
|
|
|
|
"path": USER_PATH_OUTPOSTS,
|
|
|
|
}
|
|
|
|
dirty = False
|
|
|
|
for key, value in attrs.items():
|
|
|
|
if getattr(user, key) != value:
|
|
|
|
dirty = True
|
|
|
|
setattr(user, key, value)
|
|
|
|
if user.has_usable_password():
|
|
|
|
user.set_unusable_password()
|
|
|
|
dirty = True
|
|
|
|
if dirty:
|
|
|
|
user.save()
|
2022-07-05 20:26:56 +00:00
|
|
|
if user_created:
|
2021-10-04 16:04:19 +00:00
|
|
|
self.build_user_permissions(user)
|
2020-09-02 22:04:12 +00:00
|
|
|
return user
|
|
|
|
|
2020-10-03 21:37:58 +00:00
|
|
|
@property
|
|
|
|
def token_identifier(self) -> str:
|
|
|
|
"""Get Token identifier"""
|
2020-12-05 21:08:42 +00:00
|
|
|
return f"ak-outpost-{self.pk}-api"
|
2020-10-03 21:37:58 +00:00
|
|
|
|
2020-09-02 22:04:12 +00:00
|
|
|
@property
|
|
|
|
def token(self) -> Token:
|
|
|
|
"""Get/create token for auto-generated user"""
|
2021-06-03 09:45:48 +00:00
|
|
|
managed = f"goauthentik.io/outpost/{self.token_identifier}"
|
|
|
|
tokens = Token.filter_not_expired(
|
|
|
|
identifier=self.token_identifier,
|
2021-06-02 14:04:41 +00:00
|
|
|
intent=TokenIntents.INTENT_API,
|
2021-06-03 09:45:48 +00:00
|
|
|
managed=managed,
|
2020-09-02 22:04:12 +00:00
|
|
|
)
|
2021-06-13 11:36:54 +00:00
|
|
|
if tokens.exists():
|
|
|
|
return tokens.first()
|
|
|
|
try:
|
|
|
|
return Token.objects.create(
|
|
|
|
user=self.user,
|
|
|
|
identifier=self.token_identifier,
|
|
|
|
intent=TokenIntents.INTENT_API,
|
|
|
|
description=f"Autogenerated by authentik for Outpost {self.name}",
|
|
|
|
expiring=False,
|
|
|
|
managed=managed,
|
|
|
|
)
|
|
|
|
except IntegrityError:
|
2023-10-03 12:10:10 +00:00
|
|
|
# Integrity error happens mostly when managed is reused
|
2021-06-13 11:36:54 +00:00
|
|
|
Token.objects.filter(managed=managed).delete()
|
|
|
|
Token.objects.filter(identifier=self.token_identifier).delete()
|
|
|
|
return self.token
|
2020-09-02 22:04:12 +00:00
|
|
|
|
2021-12-30 13:59:01 +00:00
|
|
|
def get_required_objects(self) -> Iterable[models.Model | str]:
|
2020-09-02 22:04:12 +00:00
|
|
|
"""Get an iterator of all objects the user needs read access to"""
|
2021-12-30 13:59:01 +00:00
|
|
|
objects: list[models.Model | str] = [
|
2021-06-26 21:37:03 +00:00
|
|
|
self,
|
|
|
|
"authentik_events.add_event",
|
|
|
|
]
|
2021-08-03 15:45:16 +00:00
|
|
|
for provider in Provider.objects.filter(outpost=self).select_related().select_subclasses():
|
2020-09-02 22:04:12 +00:00
|
|
|
if isinstance(provider, OutpostModel):
|
|
|
|
objects.extend(provider.get_required_objects())
|
|
|
|
else:
|
|
|
|
objects.append(provider)
|
2021-12-22 10:43:45 +00:00
|
|
|
if self.managed:
|
2023-11-06 11:46:14 +00:00
|
|
|
for brand in Brand.objects.filter(web_certificate__isnull=False):
|
|
|
|
objects.append(brand)
|
|
|
|
objects.append(brand.web_certificate)
|
2020-09-02 22:04:12 +00:00
|
|
|
return objects
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
return f"Outpost {self.name}"
|
2020-10-14 08:44:17 +00:00
|
|
|
|
2023-10-16 15:31:50 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("Outpost")
|
|
|
|
verbose_name_plural = _("Outposts")
|
|
|
|
|
2020-10-14 08:44:17 +00:00
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class OutpostState:
|
|
|
|
"""Outpost instance state, last_seen and version"""
|
|
|
|
|
|
|
|
uid: str
|
|
|
|
last_seen: Optional[datetime] = field(default=None)
|
|
|
|
version: Optional[str] = field(default=None)
|
2022-12-30 11:07:25 +00:00
|
|
|
version_should: Version = field(default=OUR_VERSION)
|
2021-05-12 17:05:29 +00:00
|
|
|
build_hash: str = field(default="")
|
2022-12-28 15:02:16 +00:00
|
|
|
hostname: str = field(default="")
|
2023-10-16 15:01:44 +00:00
|
|
|
args: dict = field(default_factory=dict)
|
2020-10-14 08:44:17 +00:00
|
|
|
|
|
|
|
_outpost: Optional[Outpost] = field(default=None)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def version_outdated(self) -> bool:
|
|
|
|
"""Check if outpost version matches our version"""
|
|
|
|
if not self.version:
|
|
|
|
return False
|
2022-01-13 16:47:31 +00:00
|
|
|
if self.build_hash != get_build_hash():
|
2021-05-12 17:05:29 +00:00
|
|
|
return False
|
2020-10-14 08:44:17 +00:00
|
|
|
return parse(self.version) < OUR_VERSION
|
|
|
|
|
|
|
|
@staticmethod
|
2021-02-18 12:45:46 +00:00
|
|
|
def for_outpost(outpost: Outpost) -> list["OutpostState"]:
|
2020-10-14 08:44:17 +00:00
|
|
|
"""Get all states for an outpost"""
|
2023-04-28 10:53:07 +00:00
|
|
|
keys = cache.keys(f"{outpost.state_cache_prefix}/*")
|
2021-12-20 18:44:08 +00:00
|
|
|
if not keys:
|
|
|
|
return []
|
2020-10-14 08:44:17 +00:00
|
|
|
states = []
|
|
|
|
for key in keys:
|
2023-04-28 10:53:07 +00:00
|
|
|
instance_uid = key.replace(f"{outpost.state_cache_prefix}/", "")
|
2021-05-20 13:23:18 +00:00
|
|
|
states.append(OutpostState.for_instance_uid(outpost, instance_uid))
|
2020-10-14 08:44:17 +00:00
|
|
|
return states
|
|
|
|
|
|
|
|
@staticmethod
|
2021-05-20 13:23:18 +00:00
|
|
|
def for_instance_uid(outpost: Outpost, uid: str) -> "OutpostState":
|
|
|
|
"""Get state for a single instance"""
|
2023-04-28 10:53:07 +00:00
|
|
|
key = f"{outpost.state_cache_prefix}/{uid}"
|
2023-12-30 20:33:14 +00:00
|
|
|
default_data = {"uid": uid}
|
2020-10-19 14:04:38 +00:00
|
|
|
data = cache.get(key, default_data)
|
|
|
|
if isinstance(data, str):
|
|
|
|
cache.delete(key)
|
|
|
|
data = default_data
|
2020-10-14 08:44:17 +00:00
|
|
|
state = from_dict(OutpostState, data)
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
state._outpost = outpost
|
|
|
|
return state
|
|
|
|
|
|
|
|
def save(self, timeout=OUTPOST_HELLO_INTERVAL):
|
|
|
|
"""Save current state to cache"""
|
2023-04-28 10:53:07 +00:00
|
|
|
full_key = f"{self._outpost.state_cache_prefix}/{self.uid}"
|
2020-10-14 08:44:17 +00:00
|
|
|
return cache.set(full_key, asdict(self), timeout=timeout)
|
|
|
|
|
|
|
|
def delete(self):
|
|
|
|
"""Manually delete from cache, used on channel disconnect"""
|
2023-04-28 10:53:07 +00:00
|
|
|
full_key = f"{self._outpost.state_cache_prefix}/{self.uid}"
|
2020-10-14 08:44:17 +00:00
|
|
|
cache.delete(full_key)
|