Compare commits
	
		
			6 Commits
		
	
	
		
			version-20
			...
			core/soft-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a5379c35aa | |||
| e4c11a5284 | |||
| a4853a1e09 | |||
| b65b72d910 | |||
| cd7be6a1a4 | |||
| e5cb8ef541 | 
@ -4,7 +4,6 @@ from collections.abc import Iterable
 | 
			
		||||
from uuid import UUID
 | 
			
		||||
 | 
			
		||||
from django.apps import apps
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from django.db.models import Model, Q, QuerySet
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
@ -47,8 +46,6 @@ class Exporter:
 | 
			
		||||
    def get_model_instances(self, model: type[Model]) -> QuerySet:
 | 
			
		||||
        """Return a queryset for `model`. Can be used to filter some
 | 
			
		||||
        objects on some models"""
 | 
			
		||||
        if model == get_user_model():
 | 
			
		||||
            return model.objects.exclude_anonymous()
 | 
			
		||||
        return model.objects.all()
 | 
			
		||||
 | 
			
		||||
    def _pre_export(self, blueprint: Blueprint):
 | 
			
		||||
 | 
			
		||||
@ -408,7 +408,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
    filterset_class = UsersFilter
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        base_qs = User.objects.all().exclude_anonymous()
 | 
			
		||||
        base_qs = User.objects.all()
 | 
			
		||||
        if self.serializer_class(context={"request": self.request})._should_include_groups:
 | 
			
		||||
            base_qs = base_qs.prefetch_related("ak_groups")
 | 
			
		||||
        return base_qs
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
from django.db.models import Count
 | 
			
		||||
 | 
			
		||||
import authentik.core.models
 | 
			
		||||
import authentik.lib.models
 | 
			
		||||
import authentik.lib.validators
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
			
		||||
@ -160,7 +160,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default="",
 | 
			
		||||
                validators=[authentik.lib.models.DomainlessFormattedURLValidator()],
 | 
			
		||||
                validators=[authentik.lib.validators.DomainlessFormattedURLValidator()],
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								authentik/core/migrations/0036_user_group_soft_delete.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								authentik/core/migrations/0036_user_group_soft_delete.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 5.0.4 on 2024-04-23 16:59
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("authentik_core", "0035_alter_group_options_and_more"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="group",
 | 
			
		||||
            name="deleted_at",
 | 
			
		||||
            field=models.DateTimeField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="deleted_at",
 | 
			
		||||
            field=models.DateTimeField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -28,10 +28,12 @@ from authentik.lib.avatars import get_avatar
 | 
			
		||||
from authentik.lib.generators import generate_id
 | 
			
		||||
from authentik.lib.models import (
 | 
			
		||||
    CreatedUpdatedModel,
 | 
			
		||||
    DomainlessFormattedURLValidator,
 | 
			
		||||
    SerializerModel,
 | 
			
		||||
    SoftDeleteModel,
 | 
			
		||||
    SoftDeleteQuerySet,
 | 
			
		||||
)
 | 
			
		||||
from authentik.lib.utils.time import timedelta_from_string
 | 
			
		||||
from authentik.lib.validators import DomainlessFormattedURLValidator
 | 
			
		||||
from authentik.policies.models import PolicyBindingModel
 | 
			
		||||
from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGTH
 | 
			
		||||
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
 | 
			
		||||
@ -96,7 +98,7 @@ class UserTypes(models.TextChoices):
 | 
			
		||||
    INTERNAL_SERVICE_ACCOUNT = "internal_service_account"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Group(SerializerModel):
 | 
			
		||||
class Group(SoftDeleteModel, SerializerModel):
 | 
			
		||||
    """Group model which supports a basic hierarchy and has attributes"""
 | 
			
		||||
 | 
			
		||||
    group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
			
		||||
@ -186,31 +188,21 @@ class Group(SerializerModel):
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserQuerySet(models.QuerySet):
 | 
			
		||||
    """User queryset"""
 | 
			
		||||
 | 
			
		||||
    def exclude_anonymous(self):
 | 
			
		||||
        """Exclude anonymous user"""
 | 
			
		||||
        return self.exclude(**{User.USERNAME_FIELD: settings.ANONYMOUS_USER_NAME})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserManager(DjangoUserManager):
 | 
			
		||||
    """User manager that doesn't assign is_superuser and is_staff"""
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        """Create special user queryset"""
 | 
			
		||||
        return UserQuerySet(self.model, using=self._db)
 | 
			
		||||
        return SoftDeleteQuerySet(self.model, using=self._db).exclude(
 | 
			
		||||
            **{User.USERNAME_FIELD: settings.ANONYMOUS_USER_NAME}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def create_user(self, username, email=None, password=None, **extra_fields):
 | 
			
		||||
        """User manager that doesn't assign is_superuser and is_staff"""
 | 
			
		||||
        return self._create_user(username, email, password, **extra_fields)
 | 
			
		||||
 | 
			
		||||
    def exclude_anonymous(self) -> QuerySet:
 | 
			
		||||
        """Exclude anonymous user"""
 | 
			
		||||
        return self.get_queryset().exclude_anonymous()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User(SerializerModel, GuardianUserMixin, AbstractUser):
 | 
			
		||||
class User(SoftDeleteModel, SerializerModel, GuardianUserMixin, AbstractUser):
 | 
			
		||||
    """authentik User model, based on django's contrib auth user model."""
 | 
			
		||||
 | 
			
		||||
    uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
 | 
			
		||||
 | 
			
		||||
@ -132,7 +132,7 @@ class LicenseKey:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def base_user_qs() -> QuerySet:
 | 
			
		||||
        """Base query set for all users"""
 | 
			
		||||
        return User.objects.all().exclude_anonymous().exclude(is_active=False)
 | 
			
		||||
        return User.objects.all().exclude(is_active=False)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_default_user_count():
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ from django.db import migrations, models
 | 
			
		||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
 | 
			
		||||
import authentik.events.models
 | 
			
		||||
import authentik.lib.models
 | 
			
		||||
import authentik.lib.validators
 | 
			
		||||
from authentik.lib.migrations import progress_bar
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -377,7 +377,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
            model_name="notificationtransport",
 | 
			
		||||
            name="webhook_url",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                blank=True, validators=[authentik.lib.models.DomainlessURLValidator()]
 | 
			
		||||
                blank=True, validators=[authentik.lib.validators.DomainlessURLValidator()]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -41,10 +41,11 @@ from authentik.events.utils import (
 | 
			
		||||
    sanitize_dict,
 | 
			
		||||
    sanitize_item,
 | 
			
		||||
)
 | 
			
		||||
from authentik.lib.models import DomainlessURLValidator, SerializerModel
 | 
			
		||||
from authentik.lib.models import SerializerModel
 | 
			
		||||
from authentik.lib.sentry import SentryIgnoredException
 | 
			
		||||
from authentik.lib.utils.http import get_http_session
 | 
			
		||||
from authentik.lib.utils.time import timedelta_from_string
 | 
			
		||||
from authentik.lib.validators import DomainlessURLValidator
 | 
			
		||||
from authentik.policies.models import PolicyBindingModel
 | 
			
		||||
from authentik.root.middleware import ClientIPMiddleware
 | 
			
		||||
from authentik.stages.email.utils import TemplateEmailMessage
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,16 @@
 | 
			
		||||
"""Generic models"""
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from django.core.validators import URLValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.regex_helper import _lazy_re_compile
 | 
			
		||||
from django.dispatch import Signal
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from model_utils.managers import InheritanceManager
 | 
			
		||||
from rest_framework.serializers import BaseSerializer
 | 
			
		||||
 | 
			
		||||
pre_soft_delete = Signal()
 | 
			
		||||
post_soft_delete = Signal()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SerializerModel(models.Model):
 | 
			
		||||
    """Base Abstract Model which has a serializer"""
 | 
			
		||||
@ -51,46 +54,57 @@ class InheritanceForeignKey(models.ForeignKey):
 | 
			
		||||
    forward_related_accessor_class = InheritanceForwardManyToOneDescriptor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DomainlessURLValidator(URLValidator):
 | 
			
		||||
    """Subclass of URLValidator which doesn't check the domain
 | 
			
		||||
    (to allow hostnames without domain)"""
 | 
			
		||||
class SoftDeleteQuerySet(models.QuerySet):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.host_re = "(" + self.hostname_re + self.domain_re + "|localhost)"
 | 
			
		||||
        self.regex = _lazy_re_compile(
 | 
			
		||||
            r"^(?:[a-z0-9.+-]*)://"  # scheme is validated separately
 | 
			
		||||
            r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?"  # user:pass authentication
 | 
			
		||||
            r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
 | 
			
		||||
            r"(?::\d{2,5})?"  # port
 | 
			
		||||
            r"(?:[/?#][^\s]*)?"  # resource path
 | 
			
		||||
            r"\Z",
 | 
			
		||||
            re.IGNORECASE,
 | 
			
		||||
    def delete(self):
 | 
			
		||||
        for obj in self.all():
 | 
			
		||||
            obj.delete()
 | 
			
		||||
 | 
			
		||||
    def hard_delete(self):
 | 
			
		||||
        return super().delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoftDeleteManager(models.Manager):
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return SoftDeleteQuerySet(self.model, using=self._db).filter(deleted_at__isnull=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeletedSoftDeleteManager(models.Manager):
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return super().get_queryset().exclude(deleted_at__isnull=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoftDeleteModel(models.Model):
 | 
			
		||||
    """Model which doesn't fully delete itself, but rather saved the delete status
 | 
			
		||||
    so cleanup events can run."""
 | 
			
		||||
 | 
			
		||||
    deleted_at = models.DateTimeField(blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    objects = SoftDeleteManager()
 | 
			
		||||
    deleted = DeletedSoftDeleteManager()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_deleted(self):
 | 
			
		||||
        return self.deleted_at is not None
 | 
			
		||||
 | 
			
		||||
    def delete(self, using: Any = ..., keep_parents: bool = ...) -> tuple[int, dict[str, int]]:
 | 
			
		||||
        pre_soft_delete.send(sender=self.__class__, instance=self)
 | 
			
		||||
        now = timezone.now()
 | 
			
		||||
        self.deleted_at = now
 | 
			
		||||
        self.save(
 | 
			
		||||
            update_fields=[
 | 
			
		||||
                "deleted_at",
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
        self.schemes = ["http", "https", "blank"] + list(self.schemes)
 | 
			
		||||
        post_soft_delete.send(sender=self.__class__, instance=self)
 | 
			
		||||
        return tuple()
 | 
			
		||||
 | 
			
		||||
    def __call__(self, value: str):
 | 
			
		||||
        # Check if the scheme is valid.
 | 
			
		||||
        scheme = value.split("://")[0].lower()
 | 
			
		||||
        if scheme not in self.schemes:
 | 
			
		||||
            value = "default" + value
 | 
			
		||||
        super().__call__(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DomainlessFormattedURLValidator(DomainlessURLValidator):
 | 
			
		||||
    """URL validator which allows for python format strings"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.formatter_re = r"([%\(\)a-zA-Z])*"
 | 
			
		||||
        self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)"
 | 
			
		||||
        self.regex = _lazy_re_compile(
 | 
			
		||||
            r"^(?:[a-z0-9.+-]*)://"  # scheme is validated separately
 | 
			
		||||
            r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?"  # user:pass authentication
 | 
			
		||||
            r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
 | 
			
		||||
            r"(?::\d{2,5})?"  # port
 | 
			
		||||
            r"(?:[/?#][^\s]*)?"  # resource path
 | 
			
		||||
            r"\Z",
 | 
			
		||||
            re.IGNORECASE,
 | 
			
		||||
        )
 | 
			
		||||
        self.schemes = ["http", "https", "blank"] + list(self.schemes)
 | 
			
		||||
    def force_delete(self, using: Any = ...):
 | 
			
		||||
        if not self.deleted_at:
 | 
			
		||||
            raise models.ProtectedError("Refusing to force delete non-deleted model", {self})
 | 
			
		||||
        return super().delete(using=using)
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,9 @@
 | 
			
		||||
"""Serializer validators"""
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from django.core.validators import URLValidator
 | 
			
		||||
from django.utils.regex_helper import _lazy_re_compile
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework.exceptions import ValidationError
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
@ -29,3 +33,48 @@ class RequiredTogetherValidator:
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return f"<{self.__class__.__name__}(fields={smart_repr(self.fields)})>"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DomainlessURLValidator(URLValidator):
 | 
			
		||||
    """Subclass of URLValidator which doesn't check the domain
 | 
			
		||||
    (to allow hostnames without domain)"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.host_re = "(" + self.hostname_re + self.domain_re + "|localhost)"
 | 
			
		||||
        self.regex = _lazy_re_compile(
 | 
			
		||||
            r"^(?:[a-z0-9.+-]*)://"  # scheme is validated separately
 | 
			
		||||
            r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?"  # user:pass authentication
 | 
			
		||||
            r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
 | 
			
		||||
            r"(?::\d{2,5})?"  # port
 | 
			
		||||
            r"(?:[/?#][^\s]*)?"  # resource path
 | 
			
		||||
            r"\Z",
 | 
			
		||||
            re.IGNORECASE,
 | 
			
		||||
        )
 | 
			
		||||
        self.schemes = ["http", "https", "blank"] + list(self.schemes)
 | 
			
		||||
 | 
			
		||||
    def __call__(self, value: str):
 | 
			
		||||
        # Check if the scheme is valid.
 | 
			
		||||
        scheme = value.split("://")[0].lower()
 | 
			
		||||
        if scheme not in self.schemes:
 | 
			
		||||
            value = "default" + value
 | 
			
		||||
        super().__call__(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DomainlessFormattedURLValidator(DomainlessURLValidator):
 | 
			
		||||
    """URL validator which allows for python format strings"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs) -> None:
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.formatter_re = r"([%\(\)a-zA-Z])*"
 | 
			
		||||
        self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)"
 | 
			
		||||
        self.regex = _lazy_re_compile(
 | 
			
		||||
            r"^(?:[a-z0-9.+-]*)://"  # scheme is validated separately
 | 
			
		||||
            r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?"  # user:pass authentication
 | 
			
		||||
            r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
 | 
			
		||||
            r"(?::\d{2,5})?"  # port
 | 
			
		||||
            r"(?:[/?#][^\s]*)?"  # resource path
 | 
			
		||||
            r"\Z",
 | 
			
		||||
            re.IGNORECASE,
 | 
			
		||||
        )
 | 
			
		||||
        self.schemes = ["http", "https", "blank"] + list(self.schemes)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								authentik/outposts/migrations/0022_outpost_deleted_at.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								authentik/outposts/migrations/0022_outpost_deleted_at.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 5.0.4 on 2024-04-23 21:00
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("authentik_outposts", "0021_alter_outpost_type"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="outpost",
 | 
			
		||||
            name="deleted_at",
 | 
			
		||||
            field=models.DateTimeField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -33,7 +33,7 @@ from authentik.core.models import (
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
from authentik.lib.models import InheritanceForeignKey, SerializerModel
 | 
			
		||||
from authentik.lib.models import InheritanceForeignKey, SerializerModel, SoftDeleteModel
 | 
			
		||||
from authentik.lib.sentry import SentryIgnoredException
 | 
			
		||||
from authentik.lib.utils.errors import exception_to_string
 | 
			
		||||
from authentik.outposts.controllers.k8s.utils import get_namespace
 | 
			
		||||
@ -131,7 +131,7 @@ class OutpostServiceConnection(models.Model):
 | 
			
		||||
        verbose_name = _("Outpost Service-Connection")
 | 
			
		||||
        verbose_name_plural = _("Outpost Service-Connections")
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> __version__:
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"Outpost service connection {self.name}"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@ -241,7 +241,7 @@ class KubernetesServiceConnection(SerializerModel, OutpostServiceConnection):
 | 
			
		||||
        return "ak-service-connection-kubernetes-form"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Outpost(SerializerModel, ManagedModel):
 | 
			
		||||
class Outpost(SoftDeleteModel, SerializerModel, ManagedModel):
 | 
			
		||||
    """Outpost instance which manages a service user and token"""
 | 
			
		||||
 | 
			
		||||
    uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
 | 
			
		||||
 | 
			
		||||
@ -2,13 +2,14 @@
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db.models import Model
 | 
			
		||||
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
 | 
			
		||||
from django.db.models.signals import m2m_changed, post_save, pre_save
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.brands.models import Brand
 | 
			
		||||
from authentik.core.models import Provider
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.lib.models import post_soft_delete
 | 
			
		||||
from authentik.lib.utils.reflection import class_to_path
 | 
			
		||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
 | 
			
		||||
from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save
 | 
			
		||||
@ -67,9 +68,7 @@ def post_save_update(sender, instance: Model, created: bool, **_):
 | 
			
		||||
    outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(pre_delete, sender=Outpost)
 | 
			
		||||
def pre_delete_cleanup(sender, instance: Outpost, **_):
 | 
			
		||||
@receiver(post_soft_delete, sender=Outpost)
 | 
			
		||||
def outpost_cleanup(sender, instance: Outpost, **_):
 | 
			
		||||
    """Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
 | 
			
		||||
    instance.user.delete()
 | 
			
		||||
    cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, instance)
 | 
			
		||||
    outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
 | 
			
		||||
    outpost_controller.delay(instance.pk.hex, action="down")
 | 
			
		||||
 | 
			
		||||
@ -129,17 +129,14 @@ def outpost_controller_all():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task(bind=True, base=SystemTask)
 | 
			
		||||
def outpost_controller(
 | 
			
		||||
    self: SystemTask, outpost_pk: str, action: str = "up", from_cache: bool = False
 | 
			
		||||
):
 | 
			
		||||
def outpost_controller(self: SystemTask, outpost_pk: str, action: str = "up"):
 | 
			
		||||
    """Create/update/monitor/delete the deployment of an Outpost"""
 | 
			
		||||
    logs = []
 | 
			
		||||
    if from_cache:
 | 
			
		||||
        outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
 | 
			
		||||
        LOGGER.debug("Getting outpost from cache to delete")
 | 
			
		||||
    else:
 | 
			
		||||
        outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first()
 | 
			
		||||
        LOGGER.debug("Getting outpost from DB")
 | 
			
		||||
    outpost: Outpost = None
 | 
			
		||||
    if action == "up":
 | 
			
		||||
        outpost = Outpost.objects.filter(pk=outpost_pk).first()
 | 
			
		||||
    elif action == "down":
 | 
			
		||||
        outpost = Outpost.deleted.filter(pk=outpost_pk).first()
 | 
			
		||||
    if not outpost:
 | 
			
		||||
        LOGGER.warning("No outpost")
 | 
			
		||||
        return
 | 
			
		||||
@ -155,9 +152,10 @@ def outpost_controller(
 | 
			
		||||
    except (ControllerException, ServiceConnectionInvalid) as exc:
 | 
			
		||||
        self.set_error(exc)
 | 
			
		||||
    else:
 | 
			
		||||
        if from_cache:
 | 
			
		||||
            cache.delete(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
 | 
			
		||||
        self.set_status(TaskStatus.SUCCESSFUL, *logs)
 | 
			
		||||
    finally:
 | 
			
		||||
        if outpost.deleted_at:
 | 
			
		||||
            outpost.force_delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task(bind=True, base=SystemTask)
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ from django.core.exceptions import FieldError
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
 | 
			
		||||
import authentik.lib.models
 | 
			
		||||
import authentik.lib.validators
 | 
			
		||||
import authentik.providers.proxy.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -80,7 +80,9 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    models.TextField(
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        validators=[
 | 
			
		||||
                            authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))
 | 
			
		||||
                            authentik.lib.validators.DomainlessURLValidator(
 | 
			
		||||
                                schemes=("http", "https")
 | 
			
		||||
                            )
 | 
			
		||||
                        ],
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
@ -88,7 +90,9 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    "external_host",
 | 
			
		||||
                    models.TextField(
 | 
			
		||||
                        validators=[
 | 
			
		||||
                            authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))
 | 
			
		||||
                            authentik.lib.validators.DomainlessURLValidator(
 | 
			
		||||
                                schemes=("http", "https")
 | 
			
		||||
                            )
 | 
			
		||||
                        ]
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.lib.models import DomainlessURLValidator
 | 
			
		||||
from authentik.lib.validators import DomainlessURLValidator
 | 
			
		||||
from authentik.outposts.models import OutpostModel
 | 
			
		||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -49,7 +49,7 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
 | 
			
		||||
        if type == User:
 | 
			
		||||
            # Get queryset of all users with consistent ordering
 | 
			
		||||
            # according to the provider's settings
 | 
			
		||||
            base = User.objects.all().exclude_anonymous()
 | 
			
		||||
            base = User.objects.all()
 | 
			
		||||
            if self.exclude_users_service_account:
 | 
			
		||||
                base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
 | 
			
		||||
                    type=UserTypes.INTERNAL_SERVICE_ACCOUNT
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ class SCIMGroupTests(TestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        # Delete all users and groups as the mocked HTTP responses only return one ID
 | 
			
		||||
        # which will cause errors with multiple users
 | 
			
		||||
        User.objects.all().exclude_anonymous().delete()
 | 
			
		||||
        User.objects.all().delete()
 | 
			
		||||
        Group.objects.all().delete()
 | 
			
		||||
        self.provider: SCIMProvider = SCIMProvider.objects.create(
 | 
			
		||||
            name=generate_id(),
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ class SCIMMembershipTests(TestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        # Delete all users and groups as the mocked HTTP responses only return one ID
 | 
			
		||||
        # which will cause errors with multiple users
 | 
			
		||||
        User.objects.all().exclude_anonymous().delete()
 | 
			
		||||
        User.objects.all().delete()
 | 
			
		||||
        Group.objects.all().delete()
 | 
			
		||||
        Tenant.objects.update(avatars="none")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ class SCIMUserTests(TestCase):
 | 
			
		||||
        # Delete all users and groups as the mocked HTTP responses only return one ID
 | 
			
		||||
        # which will cause errors with multiple users
 | 
			
		||||
        Tenant.objects.update(avatars="none")
 | 
			
		||||
        User.objects.all().exclude_anonymous().delete()
 | 
			
		||||
        User.objects.all().delete()
 | 
			
		||||
        Group.objects.all().delete()
 | 
			
		||||
        self.provider: SCIMProvider = SCIMProvider.objects.create(
 | 
			
		||||
            name=generate_id(),
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import django.db.models.deletion
 | 
			
		||||
from django.apps.registry import Apps
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import authentik.lib.models
 | 
			
		||||
import authentik.lib.validators
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def set_managed_flag(apps: Apps, schema_editor):
 | 
			
		||||
@ -105,7 +105,9 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    "server_uri",
 | 
			
		||||
                    models.TextField(
 | 
			
		||||
                        validators=[
 | 
			
		||||
                            authentik.lib.models.DomainlessURLValidator(schemes=["ldap", "ldaps"])
 | 
			
		||||
                            authentik.lib.validators.DomainlessURLValidator(
 | 
			
		||||
                                schemes=["ldap", "ldaps"]
 | 
			
		||||
                            )
 | 
			
		||||
                        ],
 | 
			
		||||
                        verbose_name="Server URI",
 | 
			
		||||
                    ),
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ from rest_framework.serializers import Serializer
 | 
			
		||||
from authentik.core.models import Group, PropertyMapping, Source
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
from authentik.lib.models import DomainlessURLValidator
 | 
			
		||||
from authentik.lib.validators import DomainlessURLValidator
 | 
			
		||||
 | 
			
		||||
LDAP_TIMEOUT = 15
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -161,7 +161,6 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
        self.assert_user(
 | 
			
		||||
            User.objects.exclude(username="akadmin")
 | 
			
		||||
            .exclude(username__startswith="ak-outpost")
 | 
			
		||||
            .exclude_anonymous()
 | 
			
		||||
            .exclude(pk=self.user.pk)
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
@ -244,7 +243,6 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
        self.assert_user(
 | 
			
		||||
            User.objects.exclude(username="akadmin")
 | 
			
		||||
            .exclude(username__startswith="ak-outpost")
 | 
			
		||||
            .exclude_anonymous()
 | 
			
		||||
            .exclude(pk=self.user.pk)
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
@ -314,7 +312,6 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
        self.assert_user(
 | 
			
		||||
            User.objects.exclude(username="akadmin")
 | 
			
		||||
            .exclude(username__startswith="ak-outpost")
 | 
			
		||||
            .exclude_anonymous()
 | 
			
		||||
            .exclude(pk=self.user.pk)
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user