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:
Jens Langhammer 2022-12-28 10:50:30 +01:00
parent 78f7eb4345
commit b16d1134ea
No known key found for this signature in database
6 changed files with 233 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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