core: delegated group member management (#9254)
* fix API permissions Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix group member remove notification label Signed-off-by: Jens Langhammer <jens@goauthentik.io> * consistent naming assign vs grant Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only set table search query when searching is enabled Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix hidden object permissions Signed-off-by: Jens Langhammer <jens@goauthentik.io> * replace checkmark/cross with fa icons Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update website Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests and fix permission bug Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix migrations Signed-off-by: Jens Langhammer <jens@goauthentik.io> * reword Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		@ -146,15 +146,14 @@ class GroupFilter(FilterSet):
 | 
			
		||||
        fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserAccountSerializer(PassiveSerializer):
 | 
			
		||||
    """Account adding/removing operations"""
 | 
			
		||||
 | 
			
		||||
    pk = IntegerField(required=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GroupViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
    """Group Viewset"""
 | 
			
		||||
 | 
			
		||||
    class UserAccountSerializer(PassiveSerializer):
 | 
			
		||||
        """Account adding/removing operations"""
 | 
			
		||||
 | 
			
		||||
        pk = IntegerField(required=True)
 | 
			
		||||
 | 
			
		||||
    queryset = Group.objects.all().select_related("parent").prefetch_related("users")
 | 
			
		||||
    serializer_class = GroupSerializer
 | 
			
		||||
    search_fields = ["name", "is_superuser"]
 | 
			
		||||
@ -169,7 +168,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
    def list(self, request, *args, **kwargs):
 | 
			
		||||
        return super().list(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @permission_required(None, ["authentik_core.add_user"])
 | 
			
		||||
    @permission_required("authentik_core.add_user_to_group")
 | 
			
		||||
    @extend_schema(
 | 
			
		||||
        request=UserAccountSerializer,
 | 
			
		||||
        responses={
 | 
			
		||||
@ -177,7 +176,13 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
            404: OpenApiResponse(description="User not found"),
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    @action(detail=True, methods=["POST"], pagination_class=None, filter_backends=[])
 | 
			
		||||
    @action(
 | 
			
		||||
        detail=True,
 | 
			
		||||
        methods=["POST"],
 | 
			
		||||
        pagination_class=None,
 | 
			
		||||
        filter_backends=[],
 | 
			
		||||
        permission_classes=[],
 | 
			
		||||
    )
 | 
			
		||||
    def add_user(self, request: Request, pk: str) -> Response:
 | 
			
		||||
        """Add user to group"""
 | 
			
		||||
        group: Group = self.get_object()
 | 
			
		||||
@ -193,7 +198,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
        group.users.add(user)
 | 
			
		||||
        return Response(status=204)
 | 
			
		||||
 | 
			
		||||
    @permission_required(None, ["authentik_core.add_user"])
 | 
			
		||||
    @permission_required("authentik_core.remove_user_from_group")
 | 
			
		||||
    @extend_schema(
 | 
			
		||||
        request=UserAccountSerializer,
 | 
			
		||||
        responses={
 | 
			
		||||
@ -201,7 +206,13 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
            404: OpenApiResponse(description="User not found"),
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    @action(detail=True, methods=["POST"], pagination_class=None, filter_backends=[])
 | 
			
		||||
    @action(
 | 
			
		||||
        detail=True,
 | 
			
		||||
        methods=["POST"],
 | 
			
		||||
        pagination_class=None,
 | 
			
		||||
        filter_backends=[],
 | 
			
		||||
        permission_classes=[],
 | 
			
		||||
    )
 | 
			
		||||
    def remove_user(self, request: Request, pk: str) -> Response:
 | 
			
		||||
        """Add user to group"""
 | 
			
		||||
        group: Group = self.get_object()
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
# Generated by Django 5.0.4 on 2024-04-10 19:05
 | 
			
		||||
# Generated by Django 5.0.4 on 2024-04-15 11:28
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
@ -7,11 +7,22 @@ class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("auth", "0012_alter_user_first_name_max_length"),
 | 
			
		||||
        ("authentik_core", "0033_alter_user_options"),
 | 
			
		||||
        ("authentik_core", "0034_alter_authenticatedsession_expires_and_more"),
 | 
			
		||||
        ("authentik_rbac", "0003_alter_systempermission_options"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterModelOptions(
 | 
			
		||||
            name="group",
 | 
			
		||||
            options={
 | 
			
		||||
                "permissions": [
 | 
			
		||||
                    ("add_user_to_group", "Add user to group"),
 | 
			
		||||
                    ("remove_user_from_group", "Remove user from group"),
 | 
			
		||||
                ],
 | 
			
		||||
                "verbose_name": "Group",
 | 
			
		||||
                "verbose_name_plural": "Groups",
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddIndex(
 | 
			
		||||
            model_name="group",
 | 
			
		||||
            index=models.Index(fields=["name"], name="authentik_c_name_9ba8e4_idx"),
 | 
			
		||||
@ -188,6 +188,10 @@ class Group(SerializerModel):
 | 
			
		||||
        indexes = [models.Index(fields=["name"])]
 | 
			
		||||
        verbose_name = _("Group")
 | 
			
		||||
        verbose_name_plural = _("Groups")
 | 
			
		||||
        permissions = [
 | 
			
		||||
            ("add_user_to_group", _("Add user to group")),
 | 
			
		||||
            ("remove_user_from_group", _("Remove user from group")),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserQuerySet(models.QuerySet):
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
"""Test Groups API"""
 | 
			
		||||
 | 
			
		||||
from django.urls.base import reverse
 | 
			
		||||
from guardian.shortcuts import assign_perm
 | 
			
		||||
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.core.tests.utils import create_test_user
 | 
			
		||||
from authentik.lib.generators import generate_id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,13 +13,15 @@ class TestGroupsAPI(APITestCase):
 | 
			
		||||
    """Test Groups API"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.admin = create_test_admin_user()
 | 
			
		||||
        self.login_user = create_test_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)
 | 
			
		||||
        assign_perm("authentik_core.add_user_to_group", self.login_user, group)
 | 
			
		||||
        assign_perm("authentik_core.view_user", self.login_user)
 | 
			
		||||
        self.client.force_login(self.login_user)
 | 
			
		||||
        res = self.client.post(
 | 
			
		||||
            reverse("authentik_api:group-add-user", kwargs={"pk": group.pk}),
 | 
			
		||||
            data={
 | 
			
		||||
@ -32,7 +35,9 @@ class TestGroupsAPI(APITestCase):
 | 
			
		||||
    def test_add_user_404(self):
 | 
			
		||||
        """Test add_user"""
 | 
			
		||||
        group = Group.objects.create(name=generate_id())
 | 
			
		||||
        self.client.force_login(self.admin)
 | 
			
		||||
        assign_perm("authentik_core.add_user_to_group", self.login_user, group)
 | 
			
		||||
        assign_perm("authentik_core.view_user", self.login_user)
 | 
			
		||||
        self.client.force_login(self.login_user)
 | 
			
		||||
        res = self.client.post(
 | 
			
		||||
            reverse("authentik_api:group-add-user", kwargs={"pk": group.pk}),
 | 
			
		||||
            data={
 | 
			
		||||
@ -44,8 +49,10 @@ class TestGroupsAPI(APITestCase):
 | 
			
		||||
    def test_remove_user(self):
 | 
			
		||||
        """Test remove_user"""
 | 
			
		||||
        group = Group.objects.create(name=generate_id())
 | 
			
		||||
        assign_perm("authentik_core.remove_user_from_group", self.login_user, group)
 | 
			
		||||
        assign_perm("authentik_core.view_user", self.login_user)
 | 
			
		||||
        group.users.add(self.user)
 | 
			
		||||
        self.client.force_login(self.admin)
 | 
			
		||||
        self.client.force_login(self.login_user)
 | 
			
		||||
        res = self.client.post(
 | 
			
		||||
            reverse("authentik_api:group-remove-user", kwargs={"pk": group.pk}),
 | 
			
		||||
            data={
 | 
			
		||||
@ -59,8 +66,10 @@ class TestGroupsAPI(APITestCase):
 | 
			
		||||
    def test_remove_user_404(self):
 | 
			
		||||
        """Test remove_user"""
 | 
			
		||||
        group = Group.objects.create(name=generate_id())
 | 
			
		||||
        assign_perm("authentik_core.remove_user_from_group", self.login_user, group)
 | 
			
		||||
        assign_perm("authentik_core.view_user", self.login_user)
 | 
			
		||||
        group.users.add(self.user)
 | 
			
		||||
        self.client.force_login(self.admin)
 | 
			
		||||
        self.client.force_login(self.login_user)
 | 
			
		||||
        res = self.client.post(
 | 
			
		||||
            reverse("authentik_api:group-remove-user", kwargs={"pk": group.pk}),
 | 
			
		||||
            data={
 | 
			
		||||
@ -72,11 +81,12 @@ class TestGroupsAPI(APITestCase):
 | 
			
		||||
    def test_parent_self(self):
 | 
			
		||||
        """Test parent"""
 | 
			
		||||
        group = Group.objects.create(name=generate_id())
 | 
			
		||||
        self.client.force_login(self.admin)
 | 
			
		||||
        assign_perm("view_group", self.login_user, group)
 | 
			
		||||
        assign_perm("change_group", self.login_user, group)
 | 
			
		||||
        self.client.force_login(self.login_user)
 | 
			
		||||
        res = self.client.patch(
 | 
			
		||||
            reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
 | 
			
		||||
            data={
 | 
			
		||||
                "pk": self.user.pk + 3,
 | 
			
		||||
                "parent": group.pk,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user