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

15
.vscode/settings.json vendored
View File

@ -23,15 +23,16 @@
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"yaml.customTags": [
"!Find sequence",
"!KeyOf scalar",
"!Context scalar",
"!Context sequence",
"!Format sequence",
"!Condition sequence",
"!Env sequence",
"!Context scalar",
"!Enumerate sequence",
"!Env scalar",
"!If sequence"
"!Find sequence",
"!Format sequence",
"!If sequence",
"!Index scalar",
"!KeyOf scalar",
"!Value scalar"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index",

View File

@ -113,16 +113,19 @@ class Command(BaseCommand):
)
model_path = f"{model._meta.app_label}.{model._meta.model_name}"
self.schema["properties"]["entries"]["items"]["oneOf"].append(
self.template_entry(model_path, serializer)
self.template_entry(model_path, model, serializer)
)
def template_entry(self, model_path: str, serializer: Serializer) -> dict:
def template_entry(self, model_path: str, model: type[Model], serializer: Serializer) -> dict:
"""Template entry for a single model"""
model_schema = self.to_jsonschema(serializer)
model_schema["required"] = []
def_name = f"model_{model_path}"
def_path = f"#/$defs/{def_name}"
self.schema["$defs"][def_name] = model_schema
def_name_perm = f"model_{model_path}_permissions"
def_path_perm = f"#/$defs/{def_name_perm}"
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
return {
"type": "object",
"required": ["model", "identifiers"],
@ -135,6 +138,7 @@ class Command(BaseCommand):
"default": "present",
},
"conditions": {"type": "array", "items": {"type": "boolean"}},
"permissions": {"$ref": def_path_perm},
"attrs": {"$ref": def_path},
"identifiers": {"$ref": def_path},
},
@ -185,3 +189,20 @@ class Command(BaseCommand):
if required:
result["required"] = required
return result
def model_permissions(self, model: type[Model]) -> dict:
perms = [x[0] for x in model._meta.permissions]
for action in model._meta.default_permissions:
perms.append(f"{action}_{model._meta.model_name}")
return {
"type": "array",
"items": {
"type": "object",
"required": ["permission"],
"properties": {
"permission": {"type": "string", "enum": perms},
"user": {"type": "integer"},
"role": {"type": "string"},
},
},
}

View File

@ -0,0 +1,24 @@
version: 1
entries:
- model: authentik_core.user
id: user
identifiers:
username: "%(id)s"
attrs:
name: "%(id)s"
- model: authentik_rbac.role
id: role
identifiers:
name: "%(id)s"
- model: authentik_flows.flow
identifiers:
slug: "%(id)s"
attrs:
designation: authentication
name: foo
title: foo
permissions:
- permission: view_flow
user: !KeyOf user
- permission: view_flow
role: !KeyOf role

View File

@ -0,0 +1,8 @@
version: 1
entries:
- model: authentik_rbac.role
identifiers:
name: "%(id)s"
attrs:
permissions:
- authentik_blueprints.view_blueprintinstance

View File

@ -0,0 +1,9 @@
version: 1
entries:
- model: authentik_core.user
identifiers:
username: "%(id)s"
attrs:
name: "%(id)s"
permissions:
- authentik_blueprints.view_blueprintinstance

View File

@ -0,0 +1,57 @@
"""Test blueprints v1"""
from django.test import TransactionTestCase
from guardian.shortcuts import get_perms
from authentik.blueprints.v1.importer import Importer
from authentik.core.models import User
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.rbac.models import Role
class TestBlueprintsV1RBAC(TransactionTestCase):
"""Test Blueprints rbac attribute"""
def test_user_permission(self):
"""Test permissions"""
uid = generate_id()
import_yaml = load_fixture("fixtures/rbac_user.yaml", id=uid)
importer = Importer.from_string(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
user = User.objects.filter(username=uid).first()
self.assertIsNotNone(user)
self.assertTrue(user.has_perms(["authentik_blueprints.view_blueprintinstance"]))
def test_role_permission(self):
"""Test permissions"""
uid = generate_id()
import_yaml = load_fixture("fixtures/rbac_role.yaml", id=uid)
importer = Importer.from_string(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
role = Role.objects.filter(name=uid).first()
self.assertIsNotNone(role)
self.assertEqual(
list(role.group.permissions.all().values_list("codename", flat=True)),
["view_blueprintinstance"],
)
def test_object_permission(self):
"""Test permissions"""
uid = generate_id()
import_yaml = load_fixture("fixtures/rbac_object.yaml", id=uid)
importer = Importer.from_string(import_yaml)
self.assertTrue(importer.validate()[0])
self.assertTrue(importer.apply())
flow = Flow.objects.filter(slug=uid).first()
user = User.objects.filter(username=uid).first()
role = Role.objects.filter(name=uid).first()
self.assertIsNotNone(flow)
self.assertEqual(get_perms(user, flow), ["view_flow"])
self.assertEqual(get_perms(role.group, flow), ["view_flow"])

View File

@ -1,7 +1,7 @@
"""transfer common classes"""
from collections import OrderedDict
from collections.abc import Iterable, Mapping
from collections.abc import Generator, Iterable, Mapping
from copy import copy
from dataclasses import asdict, dataclass, field, is_dataclass
from enum import Enum
@ -58,6 +58,15 @@ class BlueprintEntryDesiredState(Enum):
MUST_CREATED = "must_created"
@dataclass
class BlueprintEntryPermission:
"""Describe object-level permissions"""
permission: Union[str, "YAMLTag"]
user: Union[int, "YAMLTag", None] = field(default=None)
role: Union[str, "YAMLTag", None] = field(default=None)
@dataclass
class BlueprintEntry:
"""Single entry of a blueprint"""
@ -69,6 +78,7 @@ class BlueprintEntry:
conditions: list[Any] = field(default_factory=list)
identifiers: dict[str, Any] = field(default_factory=dict)
attrs: dict[str, Any] | None = field(default_factory=dict)
permissions: list[BlueprintEntryPermission] = field(default_factory=list)
id: str | None = None
@ -150,6 +160,17 @@ class BlueprintEntry:
"""Get the blueprint model, with yaml tags resolved if present"""
return str(self.tag_resolver(self.model, blueprint))
def get_permissions(
self, blueprint: "Blueprint"
) -> Generator[BlueprintEntryPermission, None, None]:
"""Get permissions of this entry, with all yaml tags resolved"""
for perm in self.permissions:
yield BlueprintEntryPermission(
permission=self.tag_resolver(perm.permission, blueprint),
user=self.tag_resolver(perm.user, blueprint),
role=self.tag_resolver(perm.role, blueprint),
)
def check_all_conditions_match(self, blueprint: "Blueprint") -> bool:
"""Check all conditions of this entry match (evaluate to True)"""
return all(self.tag_resolver(self.conditions, blueprint))

View File

@ -16,6 +16,7 @@ from django.db.models.query_utils import Q
from django.db.transaction import atomic
from django.db.utils import IntegrityError
from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer, Serializer
from structlog.stdlib import BoundLogger, get_logger
@ -35,6 +36,7 @@ from authentik.core.models import (
PropertyMapping,
Provider,
Source,
User,
UserSourceConnection,
)
from authentik.enterprise.license import LicenseKey
@ -54,11 +56,13 @@ from authentik.events.utils import cleanse_dict
from authentik.flows.models import FlowToken, Stage
from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.reflection import get_apps
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.rbac.models import Role
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
from authentik.tenants.models import Tenant
@ -136,6 +140,16 @@ def transaction_rollback():
pass
def rbac_models() -> dict:
models = {}
for app in get_apps():
for model in app.get_models():
if not is_model_allowed(model):
continue
models[model._meta.model_name] = app.label
return models
class Importer:
"""Import Blueprint from raw dict or YAML/JSON"""
@ -154,7 +168,10 @@ class Importer:
def default_context(self):
"""Default context"""
return {"goauthentik.io/enterprise/licensed": LicenseKey.get_total().is_valid()}
return {
"goauthentik.io/enterprise/licensed": LicenseKey.get_total().is_valid(),
"goauthentik.io/rbac/models": rbac_models(),
}
@staticmethod
def from_string(yaml_input: str, context: dict | None = None) -> "Importer":
@ -320,6 +337,15 @@ class Importer:
) from exc
return serializer
def _apply_permissions(self, instance: Model, entry: BlueprintEntry):
"""Apply object-level permissions for an entry"""
for perm in entry.get_permissions(self._import):
if perm.user is not None:
assign_perm(perm.permission, User.objects.get(pk=perm.user), instance)
if perm.role is not None:
role = Role.objects.get(pk=perm.role)
role.assign_permission(perm.permission, obj=instance)
def apply(self) -> bool:
"""Apply (create/update) models yaml, in database transaction"""
try:
@ -384,6 +410,7 @@ class Importer:
if "pk" in entry.identifiers:
self.__pk_map[entry.identifiers["pk"]] = instance.pk
entry._state = BlueprintEntryState(instance)
self._apply_permissions(instance, entry)
elif state == BlueprintEntryDesiredState.ABSENT:
instance: Model | None = serializer.instance
if instance.pk:

View File

@ -5,6 +5,7 @@ from json import loads
from typing import Any
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.db.models.functions import ExtractHour
@ -33,15 +34,21 @@ from drf_spectacular.utils import (
)
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField, ListField, SerializerMethodField
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
BooleanField,
CharField,
ChoiceField,
DateTimeField,
IntegerField,
ListField,
SerializerMethodField,
)
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
BooleanField,
DateTimeField,
ListSerializer,
PrimaryKeyRelatedField,
ValidationError,
)
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
@ -78,6 +85,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.avatars import get_avatar
from authentik.rbac.decorators import permission_required
from authentik.rbac.models import get_permission_choices
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -141,12 +149,19 @@ class UserSerializer(ModelSerializer):
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["password"] = CharField(required=False, allow_null=True)
self.fields["permissions"] = ListField(
required=False, child=ChoiceField(choices=get_permission_choices())
)
def create(self, validated_data: dict) -> User:
"""If this serializer is used in the blueprint context, we allow for
directly setting a password. However should be done via the `set_password`
method instead of directly setting it like rest_framework."""
password = validated_data.pop("password", None)
permissions = Permission.objects.filter(
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
)
validated_data["user_permissions"] = permissions
instance: User = super().create(validated_data)
self._set_password(instance, password)
return instance
@ -155,6 +170,10 @@ class UserSerializer(ModelSerializer):
"""Same as `create` above, set the password directly if we're in a blueprint
context"""
password = validated_data.pop("password", None)
permissions = Permission.objects.filter(
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
)
validated_data["user_permissions"] = permissions
instance = super().update(instance, validated_data)
self._set_password(instance, password)
return instance

View File

@ -23,6 +23,7 @@ class SAMLPropertyMappingFilter(PropertyMappingFilterSet):
class Meta(PropertyMappingFilterSet.Meta):
model = SAMLPropertyMapping
fields = PropertyMappingFilterSet.Meta.fields + ["saml_name", "friendly_name"]
class SAMLPropertyMappingViewSet(UsedByMixin, ModelViewSet):

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

View File

@ -0,0 +1,28 @@
metadata:
name: Default - RBAC - Read-only
version: 1
entries:
- model: authentik_rbac.role
identifiers:
name: authentik Read-only
id: role
attrs:
permissions: !Enumerate [
!Context goauthentik.io/rbac/models,
SEQ,
!Format [
"%s.view_%s",
!Value 0,
!Index 0,
],
]
- model: authentik_core.group
identifiers:
name: authentik Read-only
attrs:
roles:
- !KeyOf role
is_superuser: false
attributes:
notes: |
An group with an auto-generated role that allows read-only permissions on all objects.

File diff suppressed because it is too large Load Diff

View File

@ -14825,6 +14825,10 @@ paths:
operationId: propertymappings_saml_list
description: SAMLPropertyMapping Viewset
parameters:
- in: query
name: friendly_name
schema:
type: string
- in: query
name: managed
schema:
@ -14859,6 +14863,10 @@ paths:
description: Number of results to return per page.
schema:
type: integer
- in: query
name: saml_name
schema:
type: string
- name: search
required: false
in: query
@ -15953,10 +15961,6 @@ paths:
operationId: propertymappings_source_scim_list
description: SCIMSourcePropertyMapping Viewset
parameters:
- in: query
name: expression
schema:
type: string
- in: query
name: managed
schema:
@ -15965,6 +15969,10 @@ paths:
type: string
explode: true
style: form
- in: query
name: managed__isnull
schema:
type: boolean
- in: query
name: name
schema:
@ -15987,11 +15995,6 @@ paths:
description: Number of results to return per page.
schema:
type: integer
- in: query
name: pm_uuid
schema:
type: string
format: uuid
- name: search
required: false
in: query
@ -21393,7 +21396,7 @@ paths:
description: ''
/rbac/permissions/assigned_by_roles/{uuid}/assign/:
post:
operationId: rbac_permissions_assigned_by_roles_assign_create
operationId: rbac_permissions_assigned_by_roles_assign
description: |-
Assign permission(s) to role. When `object_pk` is set, the permissions
are only assigned to the specific object, otherwise they are assigned globally.
@ -21416,8 +21419,14 @@ paths:
security:
- authentik: []
responses:
'204':
description: Successfully assigned
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PermissionAssignResult'
description: ''
'400':
content:
application/json:
@ -21614,7 +21623,7 @@ paths:
description: ''
/rbac/permissions/assigned_by_users/{id}/assign/:
post:
operationId: rbac_permissions_assigned_by_users_assign_create
operationId: rbac_permissions_assigned_by_users_assign
description: Assign permission(s) to user
parameters:
- in: path
@ -21634,8 +21643,14 @@ paths:
security:
- authentik: []
responses:
'204':
description: Successfully assigned
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PermissionAssignResult'
description: ''
'400':
content:
application/json:
@ -21719,7 +21734,6 @@ paths:
schema:
type: string
format: uuid
required: true
tags:
- rbac
security:
@ -21743,6 +21757,146 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/rbac/permissions/roles/{id}/:
get:
operationId: rbac_permissions_roles_retrieve
description: Get a role's assigned object permissions
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this group object permission.
required: true
tags:
- rbac
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ExtraRoleObjectPermission'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: rbac_permissions_roles_update
description: Get a role's assigned object permissions
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this group object permission.
required: true
tags:
- rbac
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ExtraRoleObjectPermissionRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ExtraRoleObjectPermission'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: rbac_permissions_roles_partial_update
description: Get a role's assigned object permissions
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this group object permission.
required: true
tags:
- rbac
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedExtraRoleObjectPermissionRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ExtraRoleObjectPermission'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: rbac_permissions_roles_destroy
description: Get a role's assigned object permissions
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this group object permission.
required: true
tags:
- rbac
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/rbac/permissions/users/:
get:
operationId: rbac_permissions_users_list
@ -21776,7 +21930,6 @@ paths:
name: user_id
schema:
type: integer
required: true
tags:
- rbac
security:
@ -21800,6 +21953,146 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/rbac/permissions/users/{id}/:
get:
operationId: rbac_permissions_users_retrieve
description: Get a users's assigned object permissions
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this user object permission.
required: true
tags:
- rbac
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ExtraUserObjectPermission'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: rbac_permissions_users_update
description: Get a users's assigned object permissions
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this user object permission.
required: true
tags:
- rbac
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ExtraUserObjectPermissionRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ExtraUserObjectPermission'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: rbac_permissions_users_partial_update
description: Get a users's assigned object permissions
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this user object permission.
required: true
tags:
- rbac
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedExtraUserObjectPermissionRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ExtraUserObjectPermission'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: rbac_permissions_users_destroy
description: Get a users's assigned object permissions
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this user object permission.
required: true
tags:
- rbac
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/rbac/roles/:
get:
operationId: rbac_roles_list
@ -36544,8 +36837,6 @@ components:
readOnly: true
object_pk:
type: string
title: Object ID
readOnly: true
name:
type: string
readOnly: true
@ -36575,6 +36866,15 @@ components:
- name
- object_description
- object_pk
ExtraRoleObjectPermissionRequest:
type: object
description: User permission with additional object-related data
properties:
object_pk:
type: string
minLength: 1
required:
- object_pk
ExtraUserObjectPermission:
type: object
description: User permission with additional object-related data
@ -36594,8 +36894,6 @@ components:
readOnly: true
object_pk:
type: string
title: Object ID
readOnly: true
name:
type: string
readOnly: true
@ -36625,6 +36923,15 @@ components:
- name
- object_description
- object_pk
ExtraUserObjectPermissionRequest:
type: object
description: User permission with additional object-related data
properties:
object_pk:
type: string
minLength: 1
required:
- object_pk
FilePathRequest:
type: object
description: Serializer to upload file
@ -42500,6 +42807,20 @@ components:
expression:
type: string
minLength: 1
PatchedExtraRoleObjectPermissionRequest:
type: object
description: User permission with additional object-related data
properties:
object_pk:
type: string
minLength: 1
PatchedExtraUserObjectPermissionRequest:
type: object
description: User permission with additional object-related data
properties:
object_pk:
type: string
minLength: 1
PatchedFlowRequest:
type: object
description: Flow Serializer
@ -44497,6 +44818,14 @@ components:
minLength: 1
required:
- permissions
PermissionAssignResult:
type: object
description: Result from assigning permissions to a user/role
properties:
id:
type: string
required:
- id
PlexAuthenticationChallenge:
type: object
description: Challenge shown to the user in identification stage
@ -46317,8 +46646,6 @@ components:
readOnly: true
object_pk:
type: string
title: Object ID
readOnly: true
name:
type: string
readOnly: true
@ -49162,8 +49489,6 @@ components:
readOnly: true
object_pk:
type: string
title: Object ID
readOnly: true
name:
type: string
readOnly: true

View File

@ -53,7 +53,7 @@ export class RoleObjectPermissionForm extends ModelForm<RoleAssignData, number>
}
send(data: RoleAssignData): Promise<unknown> {
return new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByRolesAssignCreate({
return new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByRolesAssign({
uuid: data.role,
permissionAssignRequest: {
permissions: Object.keys(data.permissions).filter((key) => data.permissions[key]),

View File

@ -54,7 +54,7 @@ export class UserObjectPermissionForm extends ModelForm<UserAssignData, number>
}
send(data: UserAssignData): Promise<unknown> {
return new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByUsersAssignCreate({
return new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByUsersAssign({
id: data.user,
permissionAssignRequest: {
permissions: Object.keys(data.permissions).filter((key) => data.permissions[key]),

View File

@ -37,7 +37,7 @@ export class RolePermissionForm extends ModelForm<RolePermissionAssign, number>
}
async send(data: RolePermissionAssign) {
await new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByRolesAssignCreate({
await new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByRolesAssign({
uuid: this.roleUuid || "",
permissionAssignRequest: {
permissions: data.permissions,

View File

@ -37,7 +37,7 @@ export class UserPermissionForm extends ModelForm<UserPermissionAssign, number>
}
async send(data: UserPermissionAssign) {
await new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByUsersAssignCreate({
await new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByUsersAssign({
id: this.userId || 0,
permissionAssignRequest: {
permissions: data.permissions,

View File

@ -2,7 +2,9 @@
Some models behave differently and allow for access to different API fields when created via blueprint.
### `authentik_core.token`
## `authentik_core.token`
### `key`
:::info
Requires authentik 2023.4
@ -26,7 +28,9 @@ For example:
intent: api
```
### `authentik_core.user`
## `authentik_core.user`
### `password`
:::info
Requires authentik 2023.6
@ -49,7 +53,29 @@ For example:
password: this-should-be-a-long-value
```
### `authentik_core.application`
### `permissions`
:::info
Requires authentik 2024.8
:::
The `permissions` field can be used to set global permissions for a user. A full list of possible permissions is included in the JSON schema for blueprints.
For example:
```yaml
# [...]
- model: authentik_core.user
identifiers:
username: test-user
attrs:
permissions:
- authentik_blueprints.view_blueprintinstance
```
## `authentik_core.application`
### `icon`
:::info
Requires authentik 2023.5
@ -69,7 +95,9 @@ For example:
icon: https://goauthentik.io/img/icon.png
```
### `authentik_sources_oauth.oauthsource`, `authentik_sources_saml.samlsource`, `authentik_sources_plex.plexsource`
## `authentik_sources_oauth.oauthsource`, `authentik_sources_saml.samlsource`, `authentik_sources_plex.plexsource`
### `icon`
:::info
Requires authentik 2023.5
@ -89,7 +117,9 @@ For example:
icon: https://goauthentik.io/img/icon.png
```
### `authentik_flows.flow`
## `authentik_flows.flow`
### `icon`
:::info
Requires authentik 2023.5
@ -110,3 +140,25 @@ For example:
designation: authentication
background: https://goauthentik.io/img/icon.png
```
## `authentik_rbac.role`
### `permissions`
:::info
Requires authentik 2024.8
:::
The `permissions` field can be used to set global permissions for a role. A full list of possible permissions is included in the JSON schema for blueprints.
For example:
```yaml
# [...]
- model: authentik_rbac.role
identifiers:
name: test-role
attrs:
permissions:
- authentik_blueprints.view_blueprintinstance
```

View File

@ -60,6 +60,11 @@ entries:
designation: stage_configuration
name: default-oobe-setup
title: Welcome to authentik!
# Optionally set object-level permissions on the object
# Requires authentik 2024.8
permissions:
- permission: inspect_flow
user: !Find [authentik_core.user, [username, akadmin]]
```
## Special Labels

View File

@ -7,15 +7,15 @@ For VS Code, for example, add these entries to your `settings.json`:
```
{
"yaml.customTags": [
"!KeyOf scalar",
"!Condition sequence",
"!Context scalar",
"!Enumerate sequence",
"!Env scalar",
"!Find sequence",
"!Context scalar",
"!Format sequence",
"!If sequence",
"!Condition sequence",
"!Enumerate sequence",
"!Index scalar",
"!KeyOf scalar",
"!Value scalar"
]
}