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:
Jens L.
2024-08-02 16:34:30 +02:00
committed by GitHub
parent 3541ec467c
commit d24e2abe7f
31 changed files with 4117 additions and 77 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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