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:
Jens L
2024-01-30 01:55:26 +01:00
committed by GitHub
parent 0413afc2a8
commit 25e72558eb
15 changed files with 71 additions and 39 deletions

View File

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

View File

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

View File

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