core: add endpoints to add/remove users from group atomically
closes #4252 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
78f7eb4345
commit
b16d1134ea
|
@ -2,13 +2,20 @@
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
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, inline_serializer
|
||||||
|
from guardian.shortcuts import get_objects_for_user
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, IntegerField, JSONField
|
from rest_framework.fields import CharField, IntegerField, JSONField
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
|
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||||
|
|
||||||
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import is_dict
|
from authentik.core.api.utils import is_dict
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
|
@ -134,3 +141,63 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||||
if self.request.user.has_perm("authentik_core.view_group"):
|
if self.request.user.has_perm("authentik_core.view_group"):
|
||||||
return self._filter_queryset_for_list(queryset)
|
return self._filter_queryset_for_list(queryset)
|
||||||
return super().filter_queryset(queryset)
|
return super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
@permission_required(None, ["authentik_core.add_user"])
|
||||||
|
@extend_schema(
|
||||||
|
request=inline_serializer(
|
||||||
|
"UserAccountSerializer",
|
||||||
|
{
|
||||||
|
"pk": IntegerField(required=True),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
204: OpenApiResponse(description="User added"),
|
||||||
|
404: OpenApiResponse(description="User not found"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=["POST"], pagination_class=None, filter_backends=[])
|
||||||
|
# pylint: disable=unused-argument, invalid-name
|
||||||
|
def add_user(self, request: Request, pk: str) -> Response:
|
||||||
|
"""Add user to group"""
|
||||||
|
group: Group = self.get_object()
|
||||||
|
user: User = (
|
||||||
|
get_objects_for_user(request.user, "authentik_core.view_user")
|
||||||
|
.filter(
|
||||||
|
pk=request.data.get("pk"),
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
raise Http404
|
||||||
|
group.users.add(user)
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
@permission_required(None, ["authentik_core.add_user"])
|
||||||
|
@extend_schema(
|
||||||
|
request=inline_serializer(
|
||||||
|
"UserAccountSerializer",
|
||||||
|
{
|
||||||
|
"pk": IntegerField(required=True),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
204: OpenApiResponse(description="User added"),
|
||||||
|
404: OpenApiResponse(description="User not found"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=["POST"], pagination_class=None, filter_backends=[])
|
||||||
|
# pylint: disable=unused-argument, invalid-name
|
||||||
|
def remove_user(self, request: Request, pk: str) -> Response:
|
||||||
|
"""Add user to group"""
|
||||||
|
group: Group = self.get_object()
|
||||||
|
user: User = (
|
||||||
|
get_objects_for_user(request.user, "authentik_core.view_user")
|
||||||
|
.filter(
|
||||||
|
pk=request.data.get("pk"),
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
raise Http404
|
||||||
|
group.users.remove(user)
|
||||||
|
return Response(status=204)
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
"""Test Groups API"""
|
||||||
|
from django.urls.base import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroupsAPI(APITestCase):
|
||||||
|
"""Test Groups API"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.admin = create_test_admin_user()
|
||||||
|
self.user = User.objects.create(username="test-user")
|
||||||
|
|
||||||
|
def test_add_user(self):
|
||||||
|
"""Test add_user"""
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
res = self.client.post(
|
||||||
|
reverse("authentik_api:group-add-user", kwargs={"pk": group.pk}),
|
||||||
|
data={
|
||||||
|
"pk": self.user.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 204)
|
||||||
|
group.refresh_from_db()
|
||||||
|
self.assertEqual(list(group.users.all()), [self.user])
|
||||||
|
|
||||||
|
def test_add_user_404(self):
|
||||||
|
"""Test add_user"""
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
res = self.client.post(
|
||||||
|
reverse("authentik_api:group-add-user", kwargs={"pk": group.pk}),
|
||||||
|
data={
|
||||||
|
"pk": self.user.pk + 3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 404)
|
||||||
|
|
||||||
|
def test_remove_user(self):
|
||||||
|
"""Test remove_user"""
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
group.users.add(self.user)
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
res = self.client.post(
|
||||||
|
reverse("authentik_api:group-remove-user", kwargs={"pk": group.pk}),
|
||||||
|
data={
|
||||||
|
"pk": self.user.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 204)
|
||||||
|
group.refresh_from_db()
|
||||||
|
self.assertEqual(list(group.users.all()), [])
|
||||||
|
|
||||||
|
def test_remove_user_404(self):
|
||||||
|
"""Test remove_user"""
|
||||||
|
group = Group.objects.create(name=generate_id())
|
||||||
|
group.users.add(self.user)
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
res = self.client.post(
|
||||||
|
reverse("authentik_api:group-remove-user", kwargs={"pk": group.pk}),
|
||||||
|
data={
|
||||||
|
"pk": self.user.pk + 3,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 404)
|
85
schema.yml
85
schema.yml
|
@ -3458,6 +3458,84 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
description: ''
|
description: ''
|
||||||
|
/core/groups/{group_uuid}/add_user/:
|
||||||
|
post:
|
||||||
|
operationId: core_groups_add_user_create
|
||||||
|
description: Add user to group
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: group_uuid
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: A UUID string identifying this group.
|
||||||
|
required: true
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserAccountRequest'
|
||||||
|
required: true
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: User added
|
||||||
|
'404':
|
||||||
|
description: User not found
|
||||||
|
'400':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
description: ''
|
||||||
|
'403':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
description: ''
|
||||||
|
/core/groups/{group_uuid}/remove_user/:
|
||||||
|
post:
|
||||||
|
operationId: core_groups_remove_user_create
|
||||||
|
description: Add user to group
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: group_uuid
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: A UUID string identifying this group.
|
||||||
|
required: true
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserAccountRequest'
|
||||||
|
required: true
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: User added
|
||||||
|
'404':
|
||||||
|
description: User not found
|
||||||
|
'400':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
description: ''
|
||||||
|
'403':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
description: ''
|
||||||
/core/groups/{group_uuid}/used_by/:
|
/core/groups/{group_uuid}/used_by/:
|
||||||
get:
|
get:
|
||||||
operationId: core_groups_used_by_list
|
operationId: core_groups_used_by_list
|
||||||
|
@ -37825,6 +37903,13 @@ components:
|
||||||
- pk
|
- pk
|
||||||
- uid
|
- uid
|
||||||
- username
|
- username
|
||||||
|
UserAccountRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pk:
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- pk
|
||||||
UserConsent:
|
UserConsent:
|
||||||
type: object
|
type: object
|
||||||
description: UserConsent Serializer
|
description: UserConsent Serializer
|
||||||
|
|
|
@ -52,17 +52,15 @@ export class RelatedGroupList extends Table<Group> {
|
||||||
return html`<ak-forms-delete-bulk
|
return html`<ak-forms-delete-bulk
|
||||||
objectLabel=${t`Group(s)`}
|
objectLabel=${t`Group(s)`}
|
||||||
actionLabel=${t`Remove from Group(s)`}
|
actionLabel=${t`Remove from Group(s)`}
|
||||||
actionSubtext=${t`Are you sure you want to remove users ${this.targetUser?.username} from the following groups?`}
|
actionSubtext=${t`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`}
|
||||||
.objects=${this.selectedElements}
|
.objects=${this.selectedElements}
|
||||||
.delete=${(item: Group) => {
|
.delete=${(item: Group) => {
|
||||||
const newGroups = this.targetUser?.groups.filter((group) => {
|
if (!this.targetUser) return;
|
||||||
return group != item.pk;
|
return new CoreApi(DEFAULT_CONFIG).coreGroupsRemoveUserCreate({
|
||||||
});
|
groupUuid: item.pk,
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
|
userAccountRequest: {
|
||||||
id: this.targetUser?.pk || 0,
|
pk: this.targetUser?.pk || 0,
|
||||||
patchedUserRequest: {
|
}
|
||||||
groups: newGroups,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -88,16 +88,11 @@ export class RelatedUserList extends Table<User> {
|
||||||
];
|
];
|
||||||
}}
|
}}
|
||||||
.delete=${(item: User) => {
|
.delete=${(item: User) => {
|
||||||
const newUsers = this.targetGroup?.usersObj
|
return new CoreApi(DEFAULT_CONFIG).coreGroupsRemoveUserCreate({
|
||||||
.filter((user) => {
|
|
||||||
return user.pk != item.pk;
|
|
||||||
})
|
|
||||||
.map((user) => user.pk);
|
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsPartialUpdate({
|
|
||||||
groupUuid: this.targetGroup?.pk || "",
|
groupUuid: this.targetGroup?.pk || "",
|
||||||
patchedGroupRequest: {
|
userAccountRequest: {
|
||||||
users: newUsers,
|
pk: item.pk,
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -88,7 +88,7 @@ image:
|
||||||
- stages/invitation: fix incorrect pk check for invitation's flow
|
- stages/invitation: fix incorrect pk check for invitation's flow
|
||||||
- stages/user_login: prevent double success message when logging in via source
|
- stages/user_login: prevent double success message when logging in via source
|
||||||
- stages/user_write: always ignore `component` field and prevent warning
|
- stages/user_write: always ignore `component` field and prevent warning
|
||||||
- web: fix authentification with Plex on iOS (#4095)
|
- web: fix authentication with Plex on iOS (#4095)
|
||||||
- web: ignore d3 circular deps warning, treat unresolved import as error
|
- web: ignore d3 circular deps warning, treat unresolved import as error
|
||||||
- web: use version family subdomain for in-app doc links
|
- web: use version family subdomain for in-app doc links
|
||||||
- web/admin: better show metadata download for saml provider
|
- web/admin: better show metadata download for saml provider
|
||||||
|
|
Reference in New Issue