Compare commits

...

6 Commits

Author SHA1 Message Date
a5379c35aa add to user
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 18:00:00 +02:00
e4c11a5284 manager for deleted objects
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:59:06 +02:00
a4853a1e09 migrate outpost to soft-delete
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:59:06 +02:00
b65b72d910 core: exclude anonymous user by default
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:59:06 +02:00
cd7be6a1a4 initial soft delete
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:58:03 +02:00
e5cb8ef541 unrelated reorganization
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:58:01 +02:00
23 changed files with 197 additions and 103 deletions

View File

@ -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):

View File

@ -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

View File

@ -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(

View 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),
),
]

View File

@ -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)

View File

@ -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():

View File

@ -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()]
), ),
), ),
] ]

View File

@ -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

View File

@ -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)

View File

@ -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)

View 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),
),
]

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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")
)
] ]
), ),
), ),

View File

@ -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

View File

@ -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

View File

@ -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(),

View File

@ -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")

View File

@ -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(),

View File

@ -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",
), ),

View File

@ -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

View File

@ -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()
) )