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.request import Request
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 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.tasks import apply_blueprint, blueprints_find_dict
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

View File

@ -20,6 +20,8 @@ from rest_framework.serializers import (
raise_errors_on_nested_writes,
)
from authentik.rbac.permissions import assign_initial_permissions
def is_dict(value: Any):
"""Ensure a value is a dictionary, useful for JSONFields"""
@ -29,6 +31,14 @@ def is_dict(value: Any):
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):
raise_errors_on_nested_writes("update", self, validated_data)

View File

@ -1,9 +1,17 @@
"""Test API Utils"""
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 authentik.core.api.utils import ModelSerializer as CustomModelSerializer
from authentik.core.api.utils import is_dict
from authentik.lib.utils.reflection import all_subclasses
class TestAPIUtils(APITestCase):
@ -14,3 +22,14 @@ class TestAPIUtils(APITestCase):
self.assertIsNone(is_dict({}))
with self.assertRaises(ValidationError):
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.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
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 (
DeliveryMethods,
EventTypes,

View File

@ -2,11 +2,11 @@
from rest_framework import mixins
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
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 django.contrib.auth.management import _get_all_permissions
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 _
@ -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):
"""System-wide permissions that are not related to any direct
database model"""

View File

@ -1,9 +1,13 @@
"""RBAC Permissions"""
from django.contrib.contenttypes.models import ContentType
from django.db.models import Model
from guardian.shortcuts import assign_perm
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from rest_framework.request import Request
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode
class ObjectPermissions(DjangoObjectPermissions):
"""RBAC Permissions"""
@ -51,3 +55,20 @@ def HasPermission(*perm: str) -> type[BasePermission]:
return bool(request.user and request.user.has_perms(perm))
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"""
from authentik.rbac.api.initial_permissions import InitialPermissionsViewSet
from authentik.rbac.api.rbac import RBACPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_roles import RoleAssignedPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_users import UserAssignedPermissionViewSet
@ -21,5 +22,6 @@ api_urlpatterns = [
("rbac/permissions/users", UserPermissionViewSet, "permissions-users"),
("rbac/permissions/roles", RolePermissionViewSet, "permissions-roles"),
("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
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.request import Request
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.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.flows.api.stages import StageSerializer
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.request import Request
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.viewsets import ModelViewSet
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.lib.config import CONFIG
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",
"required": [
@ -4828,6 +4868,7 @@
"authentik_providers_scim.scimprovider",
"authentik_providers_scim.scimmapping",
"authentik_rbac.role",
"authentik_rbac.initialpermissions",
"authentik_sources_kerberos.kerberossource",
"authentik_sources_kerberos.kerberossourcepropertymapping",
"authentik_sources_kerberos.userkerberossourceconnection",
@ -7169,12 +7210,16 @@
"authentik_providers_ssf.view_stream",
"authentik_providers_ssf.view_streamevent",
"authentik_rbac.access_admin_interface",
"authentik_rbac.add_initialpermissions",
"authentik_rbac.add_role",
"authentik_rbac.assign_role_permissions",
"authentik_rbac.change_initialpermissions",
"authentik_rbac.change_role",
"authentik_rbac.delete_initialpermissions",
"authentik_rbac.delete_role",
"authentik_rbac.edit_system_settings",
"authentik_rbac.unassign_role_permissions",
"authentik_rbac.view_initialpermissions",
"authentik_rbac.view_role",
"authentik_rbac.view_system_info",
"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": {
"type": "object",
"properties": {
@ -13793,12 +13896,16 @@
"authentik_providers_ssf.view_stream",
"authentik_providers_ssf.view_streamevent",
"authentik_rbac.access_admin_interface",
"authentik_rbac.add_initialpermissions",
"authentik_rbac.add_role",
"authentik_rbac.assign_role_permissions",
"authentik_rbac.change_initialpermissions",
"authentik_rbac.change_role",
"authentik_rbac.delete_initialpermissions",
"authentik_rbac.delete_role",
"authentik_rbac.edit_system_settings",
"authentik_rbac.unassign_role_permissions",
"authentik_rbac.view_initialpermissions",
"authentik_rbac.view_role",
"authentik_rbac.view_system_info",
"authentik_rbac.view_system_settings",

View File

@ -24209,6 +24209,270 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
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/:
get:
operationId: rbac_permissions_list
@ -24370,6 +24634,7 @@ paths:
- authentik_providers_scim.scimmapping
- authentik_providers_scim.scimprovider
- authentik_providers_ssf.ssfprovider
- authentik_rbac.initialpermissions
- authentik_rbac.role
- authentik_sources_kerberos.groupkerberossourceconnection
- authentik_sources_kerberos.kerberossource
@ -24616,6 +24881,7 @@ paths:
- authentik_providers_scim.scimmapping
- authentik_providers_scim.scimprovider
- authentik_providers_ssf.ssfprovider
- authentik_rbac.initialpermissions
- authentik_rbac.role
- authentik_sources_kerberos.groupkerberossourceconnection
- authentik_sources_kerberos.kerberossource
@ -45974,6 +46240,63 @@ components:
minLength: 1
required:
- 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:
type: object
properties:
@ -47662,6 +47985,7 @@ components:
- authentik_providers_scim.scimprovider
- authentik_providers_scim.scimmapping
- authentik_rbac.role
- authentik_rbac.initialpermissions
- authentik_sources_kerberos.kerberossource
- authentik_sources_kerberos.kerberossourcepropertymapping
- authentik_sources_kerberos.userkerberossourceconnection
@ -49390,6 +49714,18 @@ components:
required:
- pagination
- results
PaginatedInitialPermissionsList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/InitialPermissions'
required:
- pagination
- results
PaginatedInvitationList:
type: object
properties:
@ -51939,6 +52275,23 @@ components:
type: boolean
description: When enabled, the stage will succeed and continue even when
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:
type: object
description: Invitation Serializer
@ -54104,6 +54457,21 @@ components:
type: string
required:
- 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:
type: object
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/groups", msg("Groups"), [`^/identity/groups/(?<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/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]],

View File

@ -84,6 +84,10 @@ export const ROUTES: Route[] = [
await import("@goauthentik/admin/roles/RoleListPage");
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) => {
await import("@goauthentik/admin/roles/RoleViewPage");
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");
}
}