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 django.db.models.query import QuerySet
|
||||
from django.http import Http404
|
||||
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
||||
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.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
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.utils import is_dict
|
||||
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"):
|
||||
return self._filter_queryset_for_list(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:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
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/:
|
||||
get:
|
||||
operationId: core_groups_used_by_list
|
||||
|
@ -37825,6 +37903,13 @@ components:
|
|||
- pk
|
||||
- uid
|
||||
- username
|
||||
UserAccountRequest:
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
required:
|
||||
- pk
|
||||
UserConsent:
|
||||
type: object
|
||||
description: UserConsent Serializer
|
||||
|
|
|
@ -52,17 +52,15 @@ export class RelatedGroupList extends Table<Group> {
|
|||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${t`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}
|
||||
.delete=${(item: Group) => {
|
||||
const newGroups = this.targetUser?.groups.filter((group) => {
|
||||
return group != item.pk;
|
||||
});
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
|
||||
id: this.targetUser?.pk || 0,
|
||||
patchedUserRequest: {
|
||||
groups: newGroups,
|
||||
},
|
||||
if (!this.targetUser) return;
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsRemoveUserCreate({
|
||||
groupUuid: item.pk,
|
||||
userAccountRequest: {
|
||||
pk: this.targetUser?.pk || 0,
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -88,16 +88,11 @@ export class RelatedUserList extends Table<User> {
|
|||
];
|
||||
}}
|
||||
.delete=${(item: User) => {
|
||||
const newUsers = this.targetGroup?.usersObj
|
||||
.filter((user) => {
|
||||
return user.pk != item.pk;
|
||||
})
|
||||
.map((user) => user.pk);
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsPartialUpdate({
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsRemoveUserCreate({
|
||||
groupUuid: this.targetGroup?.pk || "",
|
||||
patchedGroupRequest: {
|
||||
users: newUsers,
|
||||
},
|
||||
userAccountRequest: {
|
||||
pk: item.pk,
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -88,7 +88,7 @@ image:
|
|||
- stages/invitation: fix incorrect pk check for invitation's flow
|
||||
- stages/user_login: prevent double success message when logging in via source
|
||||
- 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: use version family subdomain for in-app doc links
|
||||
- web/admin: better show metadata download for saml provider
|
||||
|
|
Reference in New Issue