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

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

View File

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

View File

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

View File

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