core: optimise user list endpoint (#8353)
* unrelated changes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * optimization pass 1: reduce N tenant lookups by taking tenant from request, reduce get_anonymous calls Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make it easier to exclude anonymous user Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix? Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -5,12 +5,12 @@ from typing import Optional
|
||||
from django.core.cache import cache
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models.functions import ExtractHour
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
@ -147,7 +147,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
],
|
||||
responses={
|
||||
200: PolicyTestResultSerializer(),
|
||||
404: OpenApiResponse(description="for_user user not found"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["GET"])
|
||||
@ -160,9 +159,11 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
for_user = request.user
|
||||
if request.user.is_superuser and "for_user" in request.query_params:
|
||||
try:
|
||||
for_user = get_object_or_404(User, pk=request.query_params.get("for_user"))
|
||||
for_user = User.objects.filter(pk=request.query_params.get("for_user")).first()
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest("for_user must be numerical")
|
||||
raise ValidationError({"for_user": "for_user must be numerical"})
|
||||
if not for_user:
|
||||
raise ValidationError({"for_user": "User not found"})
|
||||
engine = PolicyEngine(application, for_user, request)
|
||||
engine.use_cache = False
|
||||
with capture_logs() as logs:
|
||||
|
||||
@ -30,7 +30,7 @@ from drf_spectacular.utils import (
|
||||
extend_schema_field,
|
||||
inline_serializer,
|
||||
)
|
||||
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, IntegerField, ListField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
@ -72,6 +72,7 @@ from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||
from authentik.lib.avatars import get_avatar
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
@ -102,14 +103,21 @@ class UserSerializer(ModelSerializer):
|
||||
"""User Serializer"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = CharField(read_only=True)
|
||||
avatar = SerializerMethodField()
|
||||
attributes = JSONDictField(required=False)
|
||||
groups = PrimaryKeyRelatedField(
|
||||
allow_empty=True, many=True, source="ak_groups", queryset=Group.objects.all(), default=list
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
source="ak_groups",
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
default=list,
|
||||
)
|
||||
groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups")
|
||||
uid = CharField(read_only=True)
|
||||
username = CharField(max_length=150, validators=[UniqueValidator(queryset=User.objects.all())])
|
||||
username = CharField(
|
||||
max_length=150,
|
||||
validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))],
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -143,6 +151,10 @@ class UserSerializer(ModelSerializer):
|
||||
instance.set_unusable_password()
|
||||
instance.save()
|
||||
|
||||
def get_avatar(self, user: User) -> str:
|
||||
"""User's avatar, either a http/https URL or a data URI"""
|
||||
return get_avatar(user, self.context["request"])
|
||||
|
||||
def validate_path(self, path: str) -> str:
|
||||
"""Validate path"""
|
||||
if path[:1] == "/" or path[-1] == "/":
|
||||
@ -197,12 +209,16 @@ class UserSelfSerializer(ModelSerializer):
|
||||
"""User Serializer for information a user can retrieve about themselves"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = CharField(read_only=True)
|
||||
avatar = SerializerMethodField()
|
||||
groups = SerializerMethodField()
|
||||
uid = CharField(read_only=True)
|
||||
settings = SerializerMethodField()
|
||||
system_permissions = SerializerMethodField()
|
||||
|
||||
def get_avatar(self, user: User) -> str:
|
||||
"""User's avatar, either a http/https URL or a data URI"""
|
||||
return get_avatar(user, self.context["request"])
|
||||
|
||||
@extend_schema_field(
|
||||
ListSerializer(
|
||||
child=inline_serializer(
|
||||
@ -329,11 +345,11 @@ class UsersFilter(FilterSet):
|
||||
groups_by_name = ModelMultipleChoiceFilter(
|
||||
field_name="ak_groups__name",
|
||||
to_field_name="name",
|
||||
queryset=Group.objects.all(),
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
)
|
||||
groups_by_pk = ModelMultipleChoiceFilter(
|
||||
field_name="ak_groups",
|
||||
queryset=Group.objects.all(),
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
)
|
||||
|
||||
def filter_attributes(self, queryset, name, value):
|
||||
@ -378,7 +394,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
filterset_class = UsersFilter
|
||||
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
||||
return User.objects.all().exclude_anonymous().prefetch_related("ak_groups")
|
||||
|
||||
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
|
||||
"""Create a recovery link (when the current brand has a recovery flow set),
|
||||
|
||||
@ -14,6 +14,7 @@ from django.http import HttpRequest
|
||||
from django.utils.functional import SimpleLazyObject, cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from guardian.conf import settings
|
||||
from guardian.mixins import GuardianUserMixin
|
||||
from model_utils.managers import InheritanceManager
|
||||
from rest_framework.serializers import Serializer
|
||||
@ -169,13 +170,29 @@ class Group(SerializerModel):
|
||||
verbose_name_plural = _("Groups")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
"""authentik User model, based on django's contrib auth user model."""
|
||||
|
||||
Reference in New Issue
Block a user