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