rbac: rework API for terraform, add blueprint support (#10698)
* rbac: rework API slightly to improve terraform compatibility Signed-off-by: Jens Langhammer <jens@goauthentik.io> * sigh https://www.django-rest-framework.org/api-guide/filtering/#filtering-and-object-lookups Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add permission support for users global permissions Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add role support to blueprints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix yaml tags Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add generated read-only role Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix web Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make permissions optional Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add object permission support to blueprints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests kinda Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add more tests and fix bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -59,6 +59,12 @@ class PermissionSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class PermissionAssignResultSerializer(PassiveSerializer):
|
||||
"""Result from assigning permissions to a user/role"""
|
||||
|
||||
id = CharField()
|
||||
|
||||
|
||||
class PermissionFilter(FilterSet):
|
||||
"""Filter permissions"""
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.policies.event_matcher.models import model_choices
|
||||
from authentik.rbac.api.rbac import PermissionAssignSerializer
|
||||
from authentik.rbac.api.rbac import PermissionAssignResultSerializer, PermissionAssignSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.models import Role
|
||||
|
||||
@ -28,7 +28,7 @@ class RoleObjectPermissionSerializer(ModelSerializer):
|
||||
model = ReadOnlyField(source="content_type.model")
|
||||
codename = ReadOnlyField(source="permission.codename")
|
||||
name = ReadOnlyField(source="permission.name")
|
||||
object_pk = ReadOnlyField()
|
||||
object_pk = CharField()
|
||||
|
||||
class Meta:
|
||||
model = GroupObjectPermission
|
||||
@ -88,8 +88,9 @@ class RoleAssignedPermissionViewSet(ListModelMixin, GenericViewSet):
|
||||
@extend_schema(
|
||||
request=PermissionAssignSerializer(),
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully assigned"),
|
||||
200: PermissionAssignResultSerializer(many=True),
|
||||
},
|
||||
operation_id="rbac_permissions_assigned_by_roles_assign",
|
||||
)
|
||||
@action(methods=["POST"], detail=True, pagination_class=None, filter_backends=[])
|
||||
def assign(self, request: Request, *args, **kwargs) -> Response:
|
||||
@ -98,10 +99,12 @@ class RoleAssignedPermissionViewSet(ListModelMixin, GenericViewSet):
|
||||
role: Role = self.get_object()
|
||||
data = PermissionAssignSerializer(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
ids = []
|
||||
with atomic():
|
||||
for perm in data.validated_data["permissions"]:
|
||||
assign_perm(perm, role.group, data.validated_data["model_instance"])
|
||||
return Response(status=204)
|
||||
assigned_perm = assign_perm(perm, role.group, data.validated_data["model_instance"])
|
||||
ids.append(PermissionAssignResultSerializer(instance={"id": assigned_perm.pk}).data)
|
||||
return Response(ids, status=200)
|
||||
|
||||
@permission_required("authentik_rbac.unassign_role_permissions")
|
||||
@extend_schema(
|
||||
|
||||
@ -9,7 +9,7 @@ from guardian.models import UserObjectPermission
|
||||
from guardian.shortcuts import assign_perm, remove_perm
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import BooleanField, ReadOnlyField
|
||||
from rest_framework.fields import BooleanField, CharField, ReadOnlyField
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -19,7 +19,7 @@ from authentik.core.api.groups import GroupMemberSerializer
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import User, UserTypes
|
||||
from authentik.policies.event_matcher.models import model_choices
|
||||
from authentik.rbac.api.rbac import PermissionAssignSerializer
|
||||
from authentik.rbac.api.rbac import PermissionAssignResultSerializer, PermissionAssignSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ class UserObjectPermissionSerializer(ModelSerializer):
|
||||
model = ReadOnlyField(source="content_type.model")
|
||||
codename = ReadOnlyField(source="permission.codename")
|
||||
name = ReadOnlyField(source="permission.name")
|
||||
object_pk = ReadOnlyField()
|
||||
object_pk = CharField()
|
||||
|
||||
class Meta:
|
||||
model = UserObjectPermission
|
||||
@ -90,8 +90,9 @@ class UserAssignedPermissionViewSet(ListModelMixin, GenericViewSet):
|
||||
@extend_schema(
|
||||
request=PermissionAssignSerializer(),
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully assigned"),
|
||||
200: PermissionAssignResultSerializer(many=True),
|
||||
},
|
||||
operation_id="rbac_permissions_assigned_by_users_assign",
|
||||
)
|
||||
@action(methods=["POST"], detail=True, pagination_class=None, filter_backends=[])
|
||||
def assign(self, request: Request, *args, **kwargs) -> Response:
|
||||
@ -101,10 +102,12 @@ class UserAssignedPermissionViewSet(ListModelMixin, GenericViewSet):
|
||||
raise ValidationError("Permissions cannot be assigned to an internal service account.")
|
||||
data = PermissionAssignSerializer(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
ids = []
|
||||
with atomic():
|
||||
for perm in data.validated_data["permissions"]:
|
||||
assign_perm(perm, user, data.validated_data["model_instance"])
|
||||
return Response(status=204)
|
||||
assigned_perm = assign_perm(perm, user, data.validated_data["model_instance"])
|
||||
ids.append(PermissionAssignResultSerializer(instance={"id": assigned_perm.pk}).data)
|
||||
return Response(ids, status=200)
|
||||
|
||||
@permission_required("authentik_core.unassign_user_permissions")
|
||||
@extend_schema(
|
||||
|
||||
@ -6,7 +6,12 @@ from django_filters.filterset import FilterSet
|
||||
from guardian.models import GroupObjectPermission
|
||||
from guardian.shortcuts import get_objects_for_group
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.mixins import (
|
||||
DestroyModelMixin,
|
||||
ListModelMixin,
|
||||
RetrieveModelMixin,
|
||||
UpdateModelMixin,
|
||||
)
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.api.pagination import SmallerPagination
|
||||
@ -64,10 +69,12 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
|
||||
class RolePermissionFilter(FilterSet):
|
||||
"""Role permission filter"""
|
||||
|
||||
uuid = UUIDFilter("group__role__uuid", required=True)
|
||||
uuid = UUIDFilter("group__role__uuid")
|
||||
|
||||
|
||||
class RolePermissionViewSet(ListModelMixin, GenericViewSet):
|
||||
class RolePermissionViewSet(
|
||||
ListModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet
|
||||
):
|
||||
"""Get a role's assigned object permissions"""
|
||||
|
||||
serializer_class = ExtraRoleObjectPermissionSerializer
|
||||
|
||||
@ -6,7 +6,12 @@ from django_filters.filterset import FilterSet
|
||||
from guardian.models import UserObjectPermission
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.mixins import (
|
||||
DestroyModelMixin,
|
||||
ListModelMixin,
|
||||
RetrieveModelMixin,
|
||||
UpdateModelMixin,
|
||||
)
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.api.pagination import SmallerPagination
|
||||
@ -64,10 +69,12 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
|
||||
class UserPermissionFilter(FilterSet):
|
||||
"""User-assigned permission filter"""
|
||||
|
||||
user_id = NumberFilter("user__id", required=True)
|
||||
user_id = NumberFilter("user__id")
|
||||
|
||||
|
||||
class UserPermissionViewSet(ListModelMixin, GenericViewSet):
|
||||
class UserPermissionViewSet(
|
||||
ListModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet
|
||||
):
|
||||
"""Get a users's assigned object permissions"""
|
||||
|
||||
serializer_class = ExtraUserObjectPermissionSerializer
|
||||
|
||||
@ -1,15 +1,44 @@
|
||||
"""RBAC Roles"""
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from rest_framework.fields import (
|
||||
ChoiceField,
|
||||
ListField,
|
||||
)
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.rbac.models import Role
|
||||
from authentik.rbac.models import Role, get_permission_choices
|
||||
|
||||
|
||||
class RoleSerializer(ModelSerializer):
|
||||
"""Role serializer"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||
self.fields["permissions"] = ListField(
|
||||
required=False, child=ChoiceField(choices=get_permission_choices())
|
||||
)
|
||||
|
||||
def create(self, validated_data: dict) -> Role:
|
||||
permissions = Permission.objects.filter(
|
||||
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
|
||||
)
|
||||
instance: Role = super().create(validated_data)
|
||||
instance.group.permissions.set(permissions)
|
||||
return instance
|
||||
|
||||
def update(self, instance: Role, validated_data: dict) -> Role:
|
||||
permissions = Permission.objects.filter(
|
||||
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
|
||||
)
|
||||
instance: Role = super().update(instance, validated_data)
|
||||
instance.group.permissions.set(permissions)
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ["pk", "name"]
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db import models
|
||||
from django.db.transaction import atomic
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -11,6 +12,26 @@ from rest_framework.serializers import BaseSerializer
|
||||
from authentik.lib.models import SerializerModel
|
||||
|
||||
|
||||
def get_permissions():
|
||||
return (
|
||||
Permission.objects.all()
|
||||
.select_related("content_type")
|
||||
.filter(
|
||||
content_type__app_label__startswith="authentik",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_permission_choices() -> list[tuple[str, str]]:
|
||||
return [
|
||||
(
|
||||
f"{x.content_type.app_label}.{x.codename}",
|
||||
f"{x.content_type.app_label}.{x.codename}",
|
||||
)
|
||||
for x in get_permissions()
|
||||
]
|
||||
|
||||
|
||||
class Role(SerializerModel):
|
||||
"""RBAC role, which can have different permissions (both global and per-object) attached
|
||||
to it."""
|
||||
|
||||
@ -73,7 +73,7 @@ class TestRBACRoleAPI(APITestCase):
|
||||
"permissions": ["authentik_stages_invitation.view_invitation"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 204)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(self.user.has_perm("authentik_stages_invitation.view_invitation"))
|
||||
|
||||
def test_assign_object(self):
|
||||
@ -96,7 +96,7 @@ class TestRBACRoleAPI(APITestCase):
|
||||
"object_pk": str(inv.pk),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 204)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(
|
||||
self.user.has_perm(
|
||||
"authentik_stages_invitation.view_invitation",
|
||||
|
||||
@ -79,7 +79,7 @@ class TestRBACUserAPI(APITestCase):
|
||||
"permissions": ["authentik_stages_invitation.view_invitation"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 204)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(self.user.has_perm("authentik_stages_invitation.view_invitation"))
|
||||
|
||||
def test_assign_global_internal_sa(self):
|
||||
@ -121,7 +121,7 @@ class TestRBACUserAPI(APITestCase):
|
||||
"object_pk": str(inv.pk),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 204)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(
|
||||
self.user.has_perm(
|
||||
"authentik_stages_invitation.view_invitation",
|
||||
|
||||
@ -32,7 +32,7 @@ class TestRBACPermissionRoles(APITestCase):
|
||||
)
|
||||
self.role.assign_permission("authentik_stages_invitation.view_invitation", obj=inv)
|
||||
res = self.client.get(reverse("authentik_api:permissions-roles-list"))
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_list_role(self):
|
||||
"""Test list of all permissions"""
|
||||
|
||||
@ -33,7 +33,7 @@ class TestRBACPermissionUsers(APITestCase):
|
||||
)
|
||||
assign_perm("authentik_stages_invitation.view_invitation", self.user, inv)
|
||||
res = self.client.get(reverse("authentik_api:permissions-users-list"))
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_list_role(self):
|
||||
"""Test list of all permissions"""
|
||||
|
||||
Reference in New Issue
Block a user