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:
@ -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,
|
||||||
|
|||||||
@ -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"""
|
||||||
|
|||||||
@ -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"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
schema.yml
30
schema.yml
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user