core: optionally don't return groups' users and users' groups by default (#9179)

* core: don't return groups' users and users' groups by default

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* explicitly fetch users and groups in LDAP

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add indicies

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2024-04-15 13:27:44 +02:00
committed by GitHub
parent bc9984f516
commit 85fedec2f6
13 changed files with 120 additions and 30 deletions

View File

@ -5,10 +5,15 @@ from json import loads
from django.http import Http404 from django.http import Http404
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
extend_schema,
extend_schema_field,
)
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
@ -45,9 +50,7 @@ class GroupSerializer(ModelSerializer):
"""Group Serializer""" """Group Serializer"""
attributes = JSONDictField(required=False) attributes = JSONDictField(required=False)
users_obj = ListSerializer( users_obj = SerializerMethodField()
child=GroupMemberSerializer(), read_only=True, source="users", required=False
)
roles_obj = ListSerializer( roles_obj = ListSerializer(
child=RoleSerializer(), child=RoleSerializer(),
read_only=True, read_only=True,
@ -58,6 +61,19 @@ class GroupSerializer(ModelSerializer):
num_pk = IntegerField(read_only=True) num_pk = IntegerField(read_only=True)
@property
def _should_include_users(self) -> bool:
request: Request = self.context.get("request", None)
if not request:
return True
return str(request.query_params.get("include_users", "true")).lower() == "true"
@extend_schema_field(GroupMemberSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
if not self._should_include_users:
return None
return GroupMemberSerializer(instance.users, many=True).data
def validate_parent(self, parent: Group | None): def validate_parent(self, parent: Group | None):
"""Validate group parent (if set), ensuring the parent isn't itself""" """Validate group parent (if set), ensuring the parent isn't itself"""
if not self.instance or not parent: if not self.instance or not parent:
@ -145,6 +161,14 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
filterset_class = GroupFilter filterset_class = GroupFilter
ordering = ["name"] ordering = ["name"]
@extend_schema(
parameters=[
OpenApiParameter("include_users", bool, default=True),
]
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@permission_required(None, ["authentik_core.add_user"]) @permission_required(None, ["authentik_core.add_user"])
@extend_schema( @extend_schema(
request=UserAccountSerializer, request=UserAccountSerializer,

View File

@ -113,13 +113,26 @@ class UserSerializer(ModelSerializer):
queryset=Group.objects.all().order_by("name"), queryset=Group.objects.all().order_by("name"),
default=list, default=list,
) )
groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups") groups_obj = SerializerMethodField()
uid = CharField(read_only=True) uid = CharField(read_only=True)
username = CharField( username = CharField(
max_length=150, max_length=150,
validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))], validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))],
) )
@property
def _should_include_groups(self) -> bool:
request: Request = self.context.get("request", None)
if not request:
return True
return str(request.query_params.get("include_groups", "true")).lower() == "true"
@extend_schema_field(UserGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[UserGroupSerializer] | None:
if not self._should_include_groups:
return None
return UserGroupSerializer(instance.ak_groups, many=True).data
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context: if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
@ -397,6 +410,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def get_queryset(self): # pragma: no cover def get_queryset(self): # pragma: no cover
return User.objects.all().exclude_anonymous().prefetch_related("ak_groups") return User.objects.all().exclude_anonymous().prefetch_related("ak_groups")
@extend_schema(
parameters=[
OpenApiParameter("include_groups", bool, default=True),
]
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def _create_recovery_link(self) -> tuple[str, Token]: def _create_recovery_link(self) -> tuple[str, Token]:
"""Create a recovery link (when the current brand has a recovery flow set), """Create a recovery link (when the current brand has a recovery flow set),
that can either be shown to an admin or sent to the user directly""" that can either be shown to an admin or sent to the user directly"""

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.4 on 2024-04-10 19:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("authentik_core", "0033_alter_user_options"),
("authentik_rbac", "0003_alter_systempermission_options"),
]
operations = [
migrations.AddIndex(
model_name="group",
index=models.Index(fields=["name"], name="authentik_c_name_9ba8e4_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["last_login"], name="authentik_c_last_lo_f0179a_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["password_change_date"], name="authentik_c_passwor_eec915_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["uuid"], name="authentik_c_uuid_3dae2f_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["path"], name="authentik_c_path_b1f502_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["type"], name="authentik_c_type_ecf60d_idx"),
),
]

View File

@ -185,6 +185,7 @@ class Group(SerializerModel):
"parent", "parent",
), ),
) )
indexes = [models.Index(fields=["name"])]
verbose_name = _("Group") verbose_name = _("Group")
verbose_name_plural = _("Groups") verbose_name_plural = _("Groups")
@ -323,6 +324,13 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
("preview_user", _("Can preview user data sent to providers")), ("preview_user", _("Can preview user data sent to providers")),
("view_user_applications", _("View applications the user has access to")), ("view_user_applications", _("View applications the user has access to")),
] ]
indexes = [
models.Index(fields=["last_login"]),
models.Index(fields=["password_change_date"]),
models.Index(fields=["uuid"]),
models.Index(fields=["path"]),
models.Index(fields=["type"]),
]
authentik_signals_ignored_fields = [ authentik_signals_ignored_fields = [
# Logged by the events `password_set` # Logged by the events `password_set`
# the `password_set` action/signal doesn't currently convey which user # the `password_set` action/signal doesn't currently convey which user

View File

@ -113,7 +113,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
errs.Go(func() error { errs.Go(func() error {
if flags.CanSearch { if flags.CanSearch {
uapisp := sentry.StartSpan(errCtx, "authentik.providers.ldap.search.api_user") uapisp := sentry.StartSpan(errCtx, "authentik.providers.ldap.search.api_user")
searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false) searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()).IncludeGroups(true), parsedFilter, false)
if skip { if skip {
req.Log().Trace("Skip backend request") req.Log().Trace("Skip backend request")
@ -150,7 +150,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if needGroups { if needGroups {
errs.Go(func() error { errs.Go(func() error {
gapisp := sentry.StartSpan(errCtx, "authentik.providers.ldap.search.api_group") gapisp := sentry.StartSpan(errCtx, "authentik.providers.ldap.search.api_group")
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false) searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()).IncludeUsers(true), parsedFilter, false)
if skip { if skip {
req.Log().Trace("Skip backend request") req.Log().Trace("Skip backend request")
return nil return nil

View File

@ -38,8 +38,8 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
ds: direct.NewDirectSearcher(si), ds: direct.NewDirectSearcher(si),
} }
ms.log.Debug("initialised memory searcher") ms.log.Debug("initialised memory searcher")
ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO())) ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()).IncludeGroups(true))
ms.groups = paginator.FetchGroups(ms.si.GetAPIClient().CoreApi.CoreGroupsList(context.TODO())) ms.groups = paginator.FetchGroups(ms.si.GetAPIClient().CoreApi.CoreGroupsList(context.TODO()).IncludeUsers(true))
return ms return ms
} }

View File

@ -3611,6 +3611,11 @@ paths:
schema: schema:
type: string type: string
description: Attributes description: Attributes
- in: query
name: include_users
schema:
type: boolean
default: true
- in: query - in: query
name: is_superuser name: is_superuser
schema: schema:
@ -4558,6 +4563,11 @@ paths:
format: uuid format: uuid
explode: true explode: true
style: form style: form
- in: query
name: include_groups
schema:
type: boolean
default: true
- in: query - in: query
name: is_active name: is_active
schema: schema:
@ -43623,26 +43633,6 @@ components:
- num_pk - num_pk
- parent_name - parent_name
- pk - pk
UserGroupRequest:
type: object
description: Simplified Group Serializer for user's groups
properties:
name:
type: string
minLength: 1
maxLength: 80
is_superuser:
type: boolean
description: Users added to this group will be superusers.
parent:
type: string
format: uuid
nullable: true
attributes:
type: object
additionalProperties: {}
required:
- name
UserLoginChallenge: UserLoginChallenge:
type: object type: object
description: Empty challenge description: Empty challenge

View File

@ -42,6 +42,7 @@ export class GroupListPage extends TablePage<Group> {
page: page, page: page,
pageSize: (await uiConfig()).pagination.perPage, pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "", search: this.search || "",
includeUsers: false,
}); });
} }

View File

@ -33,6 +33,7 @@ export class MemberSelectTable extends TableModal<User> {
page: page, page: page,
pageSize: (await uiConfig()).pagination.perPage, pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "", search: this.search || "",
includeGroups: false,
}); });
} }

View File

@ -105,6 +105,7 @@ export class RelatedGroupList extends Table<Group> {
pageSize: (await uiConfig()).pagination.perPage, pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "", search: this.search || "",
membersByPk: this.targetUser ? [this.targetUser.pk] : [], membersByPk: this.targetUser ? [this.targetUser.pk] : [],
includeUsers: false,
}); });
} }

View File

@ -145,6 +145,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
type: this.hideServiceAccounts type: this.hideServiceAccounts
? [CoreUsersListTypeEnum.External, CoreUsersListTypeEnum.Internal] ? [CoreUsersListTypeEnum.External, CoreUsersListTypeEnum.Internal]
: undefined, : undefined,
includeGroups: false,
}); });
this.me = await me(); this.me = await me();
return users; return users;

View File

@ -38,6 +38,7 @@ export class GroupSelectModal extends TableModal<Group> {
page: page, page: page,
pageSize: (await uiConfig()).pagination.perPage, pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "", search: this.search || "",
includeUsers: false,
}); });
} }

View File

@ -146,6 +146,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
search: this.search || "", search: this.search || "",
pathStartswith: getURLParam("path", ""), pathStartswith: getURLParam("path", ""),
isActive: this.hideDeactivated ? true : undefined, isActive: this.hideDeactivated ? true : undefined,
includeGroups: false,
}); });
this.userPaths = await new CoreApi(DEFAULT_CONFIG).coreUsersPathsRetrieve({ this.userPaths = await new CoreApi(DEFAULT_CONFIG).coreUsersPathsRetrieve({
search: this.search, search: this.search,