rbac: add InitialPermissions (#13795)

* add `InitialPermissions` model to RBAC

This is a powerful construct between Permission and Role to set initial
permissions for newly created objects.

* use safer `request.user`

* fixup! use safer `request.user`

* force all self-defined serializers to descend from our custom one

See https://github.com/goauthentik/authentik/pull/10139

* reorganize initial permission assignment

* fixup! reorganize initial permission assignment
This commit is contained in:
Simonyi Gergő
2025-04-14 17:55:49 +02:00
committed by GitHub
parent 38a9e46af3
commit 03d5dad867
21 changed files with 1047 additions and 10 deletions

View File

@ -7,7 +7,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, DateTimeField from rest_framework.fields import CharField, DateTimeField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer from rest_framework.serializers import ListSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
@ -15,7 +15,7 @@ from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, PassiveSerializer from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required

View File

@ -20,6 +20,8 @@ from rest_framework.serializers import (
raise_errors_on_nested_writes, raise_errors_on_nested_writes,
) )
from authentik.rbac.permissions import assign_initial_permissions
def is_dict(value: Any): def is_dict(value: Any):
"""Ensure a value is a dictionary, useful for JSONFields""" """Ensure a value is a dictionary, useful for JSONFields"""
@ -29,6 +31,14 @@ def is_dict(value: Any):
class ModelSerializer(BaseModelSerializer): class ModelSerializer(BaseModelSerializer):
def create(self, validated_data):
instance = super().create(validated_data)
request = self.context.get("request")
if request and hasattr(request, "user") and not request.user.is_anonymous:
assign_initial_permissions(request.user, instance)
return instance
def update(self, instance: Model, validated_data): def update(self, instance: Model, validated_data):
raise_errors_on_nested_writes("update", self, validated_data) raise_errors_on_nested_writes("update", self, validated_data)

View File

@ -1,9 +1,17 @@
"""Test API Utils""" """Test API Utils"""
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.serializers import (
HyperlinkedModelSerializer,
)
from rest_framework.serializers import (
ModelSerializer as BaseModelSerializer,
)
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.api.utils import ModelSerializer as CustomModelSerializer
from authentik.core.api.utils import is_dict from authentik.core.api.utils import is_dict
from authentik.lib.utils.reflection import all_subclasses
class TestAPIUtils(APITestCase): class TestAPIUtils(APITestCase):
@ -14,3 +22,14 @@ class TestAPIUtils(APITestCase):
self.assertIsNone(is_dict({})) self.assertIsNone(is_dict({}))
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
is_dict("foo") is_dict("foo")
def test_all_serializers_descend_from_custom(self):
"""Test that every serializer we define descends from our own ModelSerializer"""
# Weirdly, there's only one serializer in `rest_framework` which descends from
# ModelSerializer: HyperlinkedModelSerializer
expected = {CustomModelSerializer, HyperlinkedModelSerializer}
actual = set(all_subclasses(BaseModelSerializer)) - set(
all_subclasses(CustomModelSerializer)
)
self.assertEqual(expected, actual)

View File

@ -4,10 +4,9 @@ from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.enterprise.providers.ssf.models import ( from authentik.enterprise.providers.ssf.models import (
DeliveryMethods, DeliveryMethods,
EventTypes, EventTypes,

View File

@ -2,11 +2,11 @@
from rest_framework import mixins from rest_framework import mixins
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
AuthenticatorEndpointGDTCStage, AuthenticatorEndpointGDTCStage,

View File

@ -0,0 +1,41 @@
"""RBAC Initial Permissions"""
from rest_framework.serializers import ListSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.rbac.api.rbac import PermissionSerializer
from authentik.rbac.models import InitialPermissions
class InitialPermissionsSerializer(ModelSerializer):
"""InitialPermissions serializer"""
permissions_obj = ListSerializer(
child=PermissionSerializer(),
read_only=True,
source="permissions",
required=False,
)
class Meta:
model = InitialPermissions
fields = [
"pk",
"name",
"mode",
"role",
"permissions",
"permissions_obj",
]
class InitialPermissionsViewSet(UsedByMixin, ModelViewSet):
"""InitialPermissions viewset"""
queryset = InitialPermissions.objects.all()
serializer_class = InitialPermissionsSerializer
search_fields = ["name"]
ordering = ["name"]
filterset_fields = ["name"]

View File

@ -0,0 +1,39 @@
# Generated by Django 5.0.13 on 2025-04-07 13:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("authentik_rbac", "0004_alter_systempermission_options"),
]
operations = [
migrations.CreateModel(
name="InitialPermissions",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.TextField(max_length=150, unique=True)),
("mode", models.CharField(choices=[("user", "User"), ("role", "Role")])),
("permissions", models.ManyToManyField(blank=True, to="auth.permission")),
(
"role",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_rbac.role"
),
),
],
options={
"verbose_name": "Initial Permissions",
"verbose_name_plural": "Initial Permissions",
},
),
]

View File

@ -3,6 +3,7 @@
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.management import _get_all_permissions from django.contrib.auth.management import _get_all_permissions
from django.contrib.auth.models import Permission
from django.db import models from django.db import models
from django.db.transaction import atomic from django.db.transaction import atomic
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -75,6 +76,35 @@ class Role(SerializerModel):
] ]
class InitialPermissionsMode(models.TextChoices):
"""Determines which entity the initial permissions are assigned to."""
USER = "user", _("User")
ROLE = "role", _("Role")
class InitialPermissions(SerializerModel):
"""Assigns permissions for newly created objects."""
name = models.TextField(max_length=150, unique=True)
mode = models.CharField(choices=InitialPermissionsMode.choices)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
permissions = models.ManyToManyField(Permission, blank=True)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.rbac.api.initial_permissions import InitialPermissionsSerializer
return InitialPermissionsSerializer
def __str__(self) -> str:
return f"Initial Permissions for Role #{self.role_id}, applying to #{self.mode}"
class Meta:
verbose_name = _("Initial Permissions")
verbose_name_plural = _("Initial Permissions")
class SystemPermission(models.Model): class SystemPermission(models.Model):
"""System-wide permissions that are not related to any direct """System-wide permissions that are not related to any direct
database model""" database model"""

View File

@ -1,9 +1,13 @@
"""RBAC Permissions""" """RBAC Permissions"""
from django.contrib.contenttypes.models import ContentType
from django.db.models import Model from django.db.models import Model
from guardian.shortcuts import assign_perm
from rest_framework.permissions import BasePermission, DjangoObjectPermissions from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from rest_framework.request import Request from rest_framework.request import Request
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode
class ObjectPermissions(DjangoObjectPermissions): class ObjectPermissions(DjangoObjectPermissions):
"""RBAC Permissions""" """RBAC Permissions"""
@ -51,3 +55,20 @@ def HasPermission(*perm: str) -> type[BasePermission]:
return bool(request.user and request.user.has_perms(perm)) return bool(request.user and request.user.has_perms(perm))
return checker return checker
# TODO: add `user: User` type annotation without circular dependencies.
# The author of this function isn't proficient/patient enough to do it.
def assign_initial_permissions(user, instance: Model):
# Performance here should not be an issue, but if needed, there are many optimization routes
initial_permissions_list = InitialPermissions.objects.filter(role__group__in=user.groups.all())
for initial_permissions in initial_permissions_list:
for permission in initial_permissions.permissions.all():
if permission.content_type != ContentType.objects.get_for_model(instance):
continue
assign_to = (
user
if initial_permissions.mode == InitialPermissionsMode.USER
else initial_permissions.role.group
)
assign_perm(permission, assign_to, instance)

View File

@ -0,0 +1,116 @@
"""Test InitialPermissions"""
from django.contrib.auth.models import Permission
from guardian.shortcuts import assign_perm
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group
from authentik.core.tests.utils import create_test_user
from authentik.lib.generators import generate_id
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode, Role
from authentik.stages.dummy.models import DummyStage
class TestInitialPermissions(APITestCase):
"""Test InitialPermissions"""
def setUp(self) -> None:
self.user = create_test_user()
self.same_role_user = create_test_user()
self.different_role_user = create_test_user()
self.role = Role.objects.create(name=generate_id())
self.different_role = Role.objects.create(name=generate_id())
self.group = Group.objects.create(name=generate_id())
self.different_group = Group.objects.create(name=generate_id())
self.group.roles.add(self.role)
self.group.users.add(self.user, self.same_role_user)
self.different_group.roles.add(self.different_role)
self.different_group.users.add(self.different_role_user)
self.ip = InitialPermissions.objects.create(
name=generate_id(), mode=InitialPermissionsMode.USER, role=self.role
)
self.view_role = Permission.objects.filter(codename="view_role").first()
self.ip.permissions.add(self.view_role)
assign_perm("authentik_rbac.add_role", self.user)
self.client.force_login(self.user)
def test_different_role(self):
"""InitialPermissions for different role does nothing"""
self.ip.role = self.different_role
self.ip.save()
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertFalse(self.user.has_perm("authentik_rbac.view_role", role))
def test_different_model(self):
"""InitialPermissions for different model does nothing"""
assign_perm("authentik_stages_dummy.add_dummystage", self.user)
self.client.post(
reverse("authentik_api:stages-dummy-list"), {"name": "test-stage", "throw-error": False}
)
role = Role.objects.filter(name="test-role").first()
self.assertFalse(self.user.has_perm("authentik_rbac.view_role", role))
stage = DummyStage.objects.filter(name="test-stage").first()
self.assertFalse(self.user.has_perm("authentik_stages_dummy.view_dummystage", stage))
def test_mode_user(self):
"""InitialPermissions adds user permission in user mode"""
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertFalse(self.same_role_user.has_perm("authentik_rbac.view_role", role))
def test_mode_role(self):
"""InitialPermissions adds role permission in role mode"""
self.ip.mode = InitialPermissionsMode.ROLE
self.ip.save()
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.same_role_user.has_perm("authentik_rbac.view_role", role))
def test_many_permissions(self):
"""InitialPermissions can add multiple permissions"""
change_role = Permission.objects.filter(codename="change_role").first()
self.ip.permissions.add(change_role)
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.user.has_perm("authentik_rbac.change_role", role))
def test_permissions_separated_by_role(self):
"""When the triggering user is part of two different roles with InitialPermissions in role
mode, it only adds permissions to the relevant role."""
self.ip.mode = InitialPermissionsMode.ROLE
self.ip.save()
different_ip = InitialPermissions.objects.create(
name=generate_id(), mode=InitialPermissionsMode.ROLE, role=self.different_role
)
change_role = Permission.objects.filter(codename="change_role").first()
different_ip.permissions.add(change_role)
self.different_group.users.add(self.user)
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.same_role_user.has_perm("authentik_rbac.view_role", role))
self.assertFalse(self.different_role_user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.user.has_perm("authentik_rbac.change_role", role))
self.assertFalse(self.same_role_user.has_perm("authentik_rbac.change_role", role))
self.assertTrue(self.different_role_user.has_perm("authentik_rbac.change_role", role))

View File

@ -1,5 +1,6 @@
"""RBAC API urls""" """RBAC API urls"""
from authentik.rbac.api.initial_permissions import InitialPermissionsViewSet
from authentik.rbac.api.rbac import RBACPermissionViewSet from authentik.rbac.api.rbac import RBACPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_roles import RoleAssignedPermissionViewSet from authentik.rbac.api.rbac_assigned_by_roles import RoleAssignedPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_users import UserAssignedPermissionViewSet from authentik.rbac.api.rbac_assigned_by_users import UserAssignedPermissionViewSet
@ -21,5 +22,6 @@ api_urlpatterns = [
("rbac/permissions/users", UserPermissionViewSet, "permissions-users"), ("rbac/permissions/users", UserPermissionViewSet, "permissions-users"),
("rbac/permissions/roles", RolePermissionViewSet, "permissions-roles"), ("rbac/permissions/roles", RolePermissionViewSet, "permissions-roles"),
("rbac/permissions", RBACPermissionViewSet), ("rbac/permissions", RBACPermissionViewSet),
("rbac/roles", RoleViewSet), ("rbac/roles", RoleViewSet, "roles"),
("rbac/initial_permissions", InitialPermissionsViewSet, "initial-permissions"),
] ]

View File

@ -2,4 +2,4 @@
from authentik.stages.dummy.api import DummyStageViewSet from authentik.stages.dummy.api import DummyStageViewSet
api_urlpatterns = [("stages/dummy", DummyStageViewSet)] api_urlpatterns = [("stages/dummy", DummyStageViewSet, "stages-dummy")]

View File

@ -4,11 +4,12 @@ from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import CharField, ModelSerializer from rest_framework.serializers import CharField
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.expression.exceptions import PropertyMappingExpressionException from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.flows.api.stages import StageSerializer from authentik.flows.api.stages import StageSerializer
from authentik.flows.challenge import HttpChallengeResponse from authentik.flows.challenge import HttpChallengeResponse

View File

@ -15,12 +15,12 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import DateTimeField, ModelSerializer from rest_framework.serializers import DateTimeField
from rest_framework.views import View from rest_framework.views import View
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authentication import validate_auth from authentik.api.authentication import validate_auth
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User from authentik.core.models import User
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.recovery.lib import create_admin_group, create_recovery_token from authentik.recovery.lib import create_admin_group, create_recovery_token

View File

@ -1201,6 +1201,46 @@
} }
} }
}, },
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_rbac.initialpermissions"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_rbac.initialpermissions_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_rbac.initialpermissions"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_rbac.initialpermissions"
}
}
},
{ {
"type": "object", "type": "object",
"required": [ "required": [
@ -4828,6 +4868,7 @@
"authentik_providers_scim.scimprovider", "authentik_providers_scim.scimprovider",
"authentik_providers_scim.scimmapping", "authentik_providers_scim.scimmapping",
"authentik_rbac.role", "authentik_rbac.role",
"authentik_rbac.initialpermissions",
"authentik_sources_kerberos.kerberossource", "authentik_sources_kerberos.kerberossource",
"authentik_sources_kerberos.kerberossourcepropertymapping", "authentik_sources_kerberos.kerberossourcepropertymapping",
"authentik_sources_kerberos.userkerberossourceconnection", "authentik_sources_kerberos.userkerberossourceconnection",
@ -7169,12 +7210,16 @@
"authentik_providers_ssf.view_stream", "authentik_providers_ssf.view_stream",
"authentik_providers_ssf.view_streamevent", "authentik_providers_ssf.view_streamevent",
"authentik_rbac.access_admin_interface", "authentik_rbac.access_admin_interface",
"authentik_rbac.add_initialpermissions",
"authentik_rbac.add_role", "authentik_rbac.add_role",
"authentik_rbac.assign_role_permissions", "authentik_rbac.assign_role_permissions",
"authentik_rbac.change_initialpermissions",
"authentik_rbac.change_role", "authentik_rbac.change_role",
"authentik_rbac.delete_initialpermissions",
"authentik_rbac.delete_role", "authentik_rbac.delete_role",
"authentik_rbac.edit_system_settings", "authentik_rbac.edit_system_settings",
"authentik_rbac.unassign_role_permissions", "authentik_rbac.unassign_role_permissions",
"authentik_rbac.view_initialpermissions",
"authentik_rbac.view_role", "authentik_rbac.view_role",
"authentik_rbac.view_system_info", "authentik_rbac.view_system_info",
"authentik_rbac.view_system_settings", "authentik_rbac.view_system_settings",
@ -7461,6 +7506,64 @@
} }
} }
}, },
"model_authentik_rbac.initialpermissions": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 150,
"minLength": 1,
"title": "Name"
},
"mode": {
"type": "string",
"enum": [
"user",
"role"
],
"title": "Mode"
},
"role": {
"type": "string",
"format": "uuid",
"title": "Role"
},
"permissions": {
"type": "array",
"items": {
"type": "integer"
},
"title": "Permissions"
}
},
"required": []
},
"model_authentik_rbac.initialpermissions_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_initialpermissions",
"change_initialpermissions",
"delete_initialpermissions",
"view_initialpermissions"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_sources_kerberos.kerberossource": { "model_authentik_sources_kerberos.kerberossource": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -13793,12 +13896,16 @@
"authentik_providers_ssf.view_stream", "authentik_providers_ssf.view_stream",
"authentik_providers_ssf.view_streamevent", "authentik_providers_ssf.view_streamevent",
"authentik_rbac.access_admin_interface", "authentik_rbac.access_admin_interface",
"authentik_rbac.add_initialpermissions",
"authentik_rbac.add_role", "authentik_rbac.add_role",
"authentik_rbac.assign_role_permissions", "authentik_rbac.assign_role_permissions",
"authentik_rbac.change_initialpermissions",
"authentik_rbac.change_role", "authentik_rbac.change_role",
"authentik_rbac.delete_initialpermissions",
"authentik_rbac.delete_role", "authentik_rbac.delete_role",
"authentik_rbac.edit_system_settings", "authentik_rbac.edit_system_settings",
"authentik_rbac.unassign_role_permissions", "authentik_rbac.unassign_role_permissions",
"authentik_rbac.view_initialpermissions",
"authentik_rbac.view_role", "authentik_rbac.view_role",
"authentik_rbac.view_system_info", "authentik_rbac.view_system_info",
"authentik_rbac.view_system_settings", "authentik_rbac.view_system_settings",

View File

@ -24209,6 +24209,270 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/rbac/initial_permissions/:
get:
operationId: rbac_initial_permissions_list
description: InitialPermissions viewset
parameters:
- in: query
name: name
schema:
type: string
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
tags:
- rbac
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedInitialPermissionsList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: rbac_initial_permissions_create
description: InitialPermissions viewset
tags:
- rbac
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InitialPermissionsRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/InitialPermissions'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/rbac/initial_permissions/{id}/:
get:
operationId: rbac_initial_permissions_retrieve
description: InitialPermissions viewset
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this Initial Permissions.
required: true
tags:
- rbac
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/InitialPermissions'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: rbac_initial_permissions_update
description: InitialPermissions viewset
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this Initial Permissions.
required: true
tags:
- rbac
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InitialPermissionsRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/InitialPermissions'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: rbac_initial_permissions_partial_update
description: InitialPermissions viewset
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this Initial Permissions.
required: true
tags:
- rbac
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedInitialPermissionsRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/InitialPermissions'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: rbac_initial_permissions_destroy
description: InitialPermissions viewset
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this Initial Permissions.
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/initial_permissions/{id}/used_by/:
get:
operationId: rbac_initial_permissions_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this Initial Permissions.
required: true
tags:
- rbac
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/rbac/permissions/: /rbac/permissions/:
get: get:
operationId: rbac_permissions_list operationId: rbac_permissions_list
@ -24370,6 +24634,7 @@ paths:
- authentik_providers_scim.scimmapping - authentik_providers_scim.scimmapping
- authentik_providers_scim.scimprovider - authentik_providers_scim.scimprovider
- authentik_providers_ssf.ssfprovider - authentik_providers_ssf.ssfprovider
- authentik_rbac.initialpermissions
- authentik_rbac.role - authentik_rbac.role
- authentik_sources_kerberos.groupkerberossourceconnection - authentik_sources_kerberos.groupkerberossourceconnection
- authentik_sources_kerberos.kerberossource - authentik_sources_kerberos.kerberossource
@ -24616,6 +24881,7 @@ paths:
- authentik_providers_scim.scimmapping - authentik_providers_scim.scimmapping
- authentik_providers_scim.scimprovider - authentik_providers_scim.scimprovider
- authentik_providers_ssf.ssfprovider - authentik_providers_ssf.ssfprovider
- authentik_rbac.initialpermissions
- authentik_rbac.role - authentik_rbac.role
- authentik_sources_kerberos.groupkerberossourceconnection - authentik_sources_kerberos.groupkerberossourceconnection
- authentik_sources_kerberos.kerberossource - authentik_sources_kerberos.kerberossource
@ -45974,6 +46240,63 @@ components:
minLength: 1 minLength: 1
required: required:
- reason - reason
InitialPermissions:
type: object
description: InitialPermissions serializer
properties:
pk:
type: integer
readOnly: true
title: ID
name:
type: string
maxLength: 150
mode:
$ref: '#/components/schemas/InitialPermissionsModeEnum'
role:
type: string
format: uuid
permissions:
type: array
items:
type: integer
permissions_obj:
type: array
items:
$ref: '#/components/schemas/Permission'
readOnly: true
required:
- mode
- name
- permissions_obj
- pk
- role
InitialPermissionsModeEnum:
enum:
- user
- role
type: string
InitialPermissionsRequest:
type: object
description: InitialPermissions serializer
properties:
name:
type: string
minLength: 1
maxLength: 150
mode:
$ref: '#/components/schemas/InitialPermissionsModeEnum'
role:
type: string
format: uuid
permissions:
type: array
items:
type: integer
required:
- mode
- name
- role
InstallID: InstallID:
type: object type: object
properties: properties:
@ -47662,6 +47985,7 @@ components:
- authentik_providers_scim.scimprovider - authentik_providers_scim.scimprovider
- authentik_providers_scim.scimmapping - authentik_providers_scim.scimmapping
- authentik_rbac.role - authentik_rbac.role
- authentik_rbac.initialpermissions
- authentik_sources_kerberos.kerberossource - authentik_sources_kerberos.kerberossource
- authentik_sources_kerberos.kerberossourcepropertymapping - authentik_sources_kerberos.kerberossourcepropertymapping
- authentik_sources_kerberos.userkerberossourceconnection - authentik_sources_kerberos.userkerberossourceconnection
@ -49390,6 +49714,18 @@ components:
required: required:
- pagination - pagination
- results - results
PaginatedInitialPermissionsList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/InitialPermissions'
required:
- pagination
- results
PaginatedInvitationList: PaginatedInvitationList:
type: object type: object
properties: properties:
@ -51939,6 +52275,23 @@ components:
type: boolean type: boolean
description: When enabled, the stage will succeed and continue even when description: When enabled, the stage will succeed and continue even when
incorrect user info is entered. incorrect user info is entered.
PatchedInitialPermissionsRequest:
type: object
description: InitialPermissions serializer
properties:
name:
type: string
minLength: 1
maxLength: 150
mode:
$ref: '#/components/schemas/InitialPermissionsModeEnum'
role:
type: string
format: uuid
permissions:
type: array
items:
type: integer
PatchedInvitationRequest: PatchedInvitationRequest:
type: object type: object
description: Invitation Serializer description: Invitation Serializer
@ -54104,6 +54457,21 @@ components:
type: string type: string
required: required:
- id - id
PermissionRequest:
type: object
description: Global permission
properties:
name:
type: string
minLength: 1
maxLength: 255
codename:
type: string
minLength: 1
maxLength: 100
required:
- codename
- name
PlexAuthenticationChallenge: PlexAuthenticationChallenge:
type: object type: object
description: Challenge shown to the user in identification stage description: Challenge shown to the user in identification stage

View File

@ -131,6 +131,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]], ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]], ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]], ["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]], ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")], ["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]], ["/flow/stages/invitations", msg("Invitations")]]],

View File

@ -84,6 +84,10 @@ export const ROUTES: Route[] = [
await import("@goauthentik/admin/roles/RoleListPage"); await import("@goauthentik/admin/roles/RoleListPage");
return html`<ak-role-list></ak-role-list>`; return html`<ak-role-list></ak-role-list>`;
}), }),
new Route(new RegExp("^/identity/initial-permissions$"), async () => {
await import("@goauthentik/admin/rbac/InitialPermissionsListPage");
return html`<ak-initial-permissions-list></ak-initial-permissions-list>`;
}),
new Route(new RegExp(`^/identity/roles/(?<id>${UUID_REGEX})$`), async (args) => { new Route(new RegExp(`^/identity/roles/(?<id>${UUID_REGEX})$`), async (args) => {
await import("@goauthentik/admin/roles/RoleViewPage"); await import("@goauthentik/admin/roles/RoleViewPage");
return html`<ak-role-view roleId=${args.id}></ak-role-view>`; return html`<ak-role-view roleId=${args.id}></ak-role-view>`;

View File

@ -0,0 +1,150 @@
import { InitialPermissionsModeToLabel } from "@goauthentik/admin/rbac/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/chips/Chip";
import "@goauthentik/elements/chips/ChipGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
InitialPermissions,
InitialPermissionsModeEnum,
Permission,
RbacApi,
RbacRolesListRequest,
Role,
} from "@goauthentik/api";
export function rbacPermissionPair(item: Permission): DualSelectPair {
return [item.id.toString(), html`<div class="selection-main">${item.name}</div>`, item.name];
}
@customElement("ak-initial-permissions-form")
export class InitialPermissionsForm extends ModelForm<InitialPermissions, string> {
loadInstance(pk: string): Promise<InitialPermissions> {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsRetrieve({
id: Number(pk),
});
}
getSuccessMessage(): string {
return this.instance
? msg("Successfully updated initial permissions.")
: msg("Successfully created initial permissions.");
}
async send(data: InitialPermissions): Promise<InitialPermissions> {
if (this.instance?.pk) {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsPartialUpdate({
id: this.instance.pk,
patchedInitialPermissionsRequest: data,
});
} else {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsCreate({
initialPermissionsRequest: data,
});
}
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Role")} required name="role">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Role[]> => {
const args: RbacRolesListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const users = await new RbacApi(DEFAULT_CONFIG).rbacRolesList(args);
return users.results;
}}
.renderElement=${(role: Role): string => {
return role.name;
}}
.renderDescription=${(role: Role): TemplateResult => {
return html`${role.name}`;
}}
.value=${(role: Role | undefined): string | undefined => {
return role?.pk;
}}
.selected=${(role: Role): boolean => {
return this.instance?.role === role.pk;
}}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"When a user with the selected Role creates an object, the Initial Permissions will be applied to that object.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<select class="pf-c-form-control">
<option
value=${InitialPermissionsModeEnum.User}
?selected=${this.instance?.mode === InitialPermissionsModeEnum.User}
>
${InitialPermissionsModeToLabel(InitialPermissionsModeEnum.User)}
</option>
<option
value=${InitialPermissionsModeEnum.Role}
?selected=${this.instance?.mode === InitialPermissionsModeEnum.Role}
>
${InitialPermissionsModeToLabel(InitialPermissionsModeEnum.Role)}
</option>
</select>
<p class="pf-c-form__helper-text">
${msg(
"The Initial Permissions can either be placed on the User creating the object, or the Role selected in the previous field.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Permissions")} name="permissions">
<ak-dual-select-provider
.provider=${(page: number, search?: string): Promise<DataProvision> => {
return new RbacApi(DEFAULT_CONFIG)
.rbacPermissionsList({
page: page,
search: search,
})
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(rbacPermissionPair),
};
});
}}
.selected=${(this.instance?.permissionsObj ?? []).map(rbacPermissionPair)}
available-label="${msg("Available Permissions")}"
selected-label="${msg("Selected Permissions")}"
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg("Permissions to grant when a new object is created.")}
</p>
</ak-form-element-horizontal>
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-initial-permissions-form": InitialPermissionsForm;
}
}

View File

@ -0,0 +1,115 @@
import "@goauthentik/admin/rbac/InitialPermissionsForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { InitialPermissions, RbacApi } from "@goauthentik/api";
@customElement("ak-initial-permissions-list")
export class InitialPermissionsListPage extends TablePage<InitialPermissions> {
checkbox = true;
clearOnRefresh = true;
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return msg("Initial Permissions");
}
pageDescription(): string {
return msg("Set initial permissions for newly created objects.");
}
pageIcon(): string {
return "fa fa-lock";
}
@property()
order = "name";
async apiEndpoint(): Promise<PaginatedResponse<InitialPermissions>> {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsList(
await this.defaultEndpointConfig(),
);
}
columns(): TableColumn[] {
return [new TableColumn(msg("Name"), "name"), new TableColumn(msg("Actions"))];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Initial Permissions")}
.objects=${this.selectedElements}
.usedBy=${(item: InitialPermissions) => {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsUsedByList({
id: item.pk,
});
}}
.delete=${(item: InitialPermissions) => {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsDestroy({
id: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
render(): TemplateResult {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}
description=${ifDefined(this.pageDescription())}
>
</ak-page-header>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">${this.renderTable()}</div>
</section>`;
}
row(item: InitialPermissions): TemplateResult[] {
return [
html`${item.name}`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Initial Permissions")} </span>
<ak-initial-permissions-form slot="form" .instancePk=${item.pk}>
</ak-initial-permissions-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Initial Permissions")} </span>
<ak-initial-permissions-form slot="form"> </ak-initial-permissions-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"initial-permissions-list": InitialPermissionsListPage;
}
}

View File

@ -0,0 +1,14 @@
import { msg } from "@lit/localize";
import { InitialPermissionsModeEnum } from "@goauthentik/api";
export function InitialPermissionsModeToLabel(mode: InitialPermissionsModeEnum): string {
switch (mode) {
case InitialPermissionsModeEnum.User:
return msg("User");
case InitialPermissionsModeEnum.Role:
return msg("Role");
case InitialPermissionsModeEnum.UnknownDefaultOpenApi:
return msg("Unknown Initial Permissions mode");
}
}