Compare commits

...

28 Commits

Author SHA1 Message Date
Jens Langhammer
b96c9cf1d6 Merge branch 'main' into core/object-attributes 2026-05-13 13:54:34 +02:00
Jens Langhammer
a555570418 Merge branch 'main' into core/object-attributes 2026-04-17 18:08:27 +02:00
Jens Langhammer
aa8463a6a8 Merge branch 'main' into core/object-attributes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	packages/client-go/api_admin.go
#	packages/client-go/api_core.go
#	packages/client-go/model_content_type.go
#	packages/client-go/model_model_enum.go
#	packages/client-rust/src/apis/admin_api.rs
#	packages/client-rust/src/apis/core_api.rs
#	packages/client-rust/src/models/content_type.rs
#	packages/client-rust/src/models/mod.rs
#	packages/client-rust/src/models/model_enum.rs
2026-04-17 01:28:48 +02:00
Jens Langhammer
e616cb8bac Merge branch 'main' into core/object-attributes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	web/src/admin/endpoints/DeviceAccessGroupForm.ts
#	web/src/admin/users/UserForm.ts
2026-04-12 01:13:04 +02:00
Jens Langhammer
ddadbba685 fixup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-12 01:08:12 +02:00
Jens Langhammer
b70dfe1cf0 fix validation for array and prevent array + unique
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 23:05:42 +02:00
Jens Langhammer
aba6932a2d start integrating attrs
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 20:49:37 +02:00
Jens Langhammer
4b66289798 make unique on enabled
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 20:38:26 +02:00
Jens Langhammer
ba6060be77 fix missing array flag
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 19:57:32 +02:00
Jens Langhammer
e835418e76 rename flags
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 19:29:08 +02:00
Jens Langhammer
e67c78ea85 add validation
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 19:25:12 +02:00
Jens Langhammer
5bbe099528 improve ui
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 18:05:07 +02:00
Jens Langhammer
949b5d671a fix managed behaviour
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 18:01:01 +02:00
Jens Langhammer
4eef34e223 start adding default user attrs
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 17:05:05 +02:00
Jens Langhammer
e58cfd3b70 remove used by
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 17:04:57 +02:00
Jens Langhammer
4ea9451e5f add group
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 16:50:57 +02:00
Jens Langhammer
d6867895aa add enabled ui
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 16:50:10 +02:00
Jens Langhammer
4727a0a69a fix form
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 16:41:03 +02:00
Jens Langhammer
1c226196b4 refactor UI
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 15:54:56 +02:00
Jens Langhammer
74f0def068 add enabled flag
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 15:49:02 +02:00
Jens Langhammer
59afc1c7d9 account for codemirror for attributes coming after a field for attributes.xyz overriding the previous field
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-11 01:11:07 +02:00
Jens Langhammer
6fda71763a fix forms
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-10 23:31:14 +02:00
Jens Langhammer
059acf477e cleanup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-10 23:12:23 +02:00
Jens Langhammer
d5b9071fa7 more fixes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-10 23:03:17 +02:00
Jens Langhammer
607b4d6a7c cleanup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-10 22:59:34 +02:00
Jens Langhammer
09cb76bf7c render raw attributes too
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-10 21:29:11 +02:00
Jens Langhammer
a4e18ba849 rework render
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-10 21:24:04 +02:00
Jens Langhammer
d70bdc68ec core: object attributes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-10 21:15:34 +02:00
37 changed files with 2670 additions and 91 deletions

View File

@@ -1,13 +1,16 @@
"""Meta API"""
from django.apps import apps
from drf_spectacular.utils import extend_schema
from rest_framework.fields import CharField
from rest_framework.fields import BooleanField, CharField
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from authentik.api.validation import validate
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import AttributesMixin
from authentik.lib.api import Models
from authentik.lib.utils.reflection import get_apps
@@ -36,12 +39,19 @@ class AppsViewSet(ViewSet):
class ModelViewSet(ViewSet):
"""Read-only view list all installed models"""
class ModelFilterSerializer(PassiveSerializer):
filter_has_attributes = BooleanField(allow_null=True, default=None)
permission_classes = [IsAuthenticated]
@extend_schema(responses={200: AppSerializer(many=True)})
def list(self, request: Request) -> Response:
@extend_schema(responses={200: AppSerializer(many=True)}, parameters=[ModelFilterSerializer])
@validate(ModelFilterSerializer, "query")
def list(self, request: Request, query: ModelFilterSerializer) -> Response:
"""Read-only view list all installed models"""
data = []
for name, label in Models.choices:
if query.validated_data["filter_has_attributes"]:
if not issubclass(apps.get_model(name), AttributesMixin):
continue
data.append({"name": name, "label": label})
return Response(AppSerializer(data, many=True).data)

View File

@@ -6,6 +6,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_attributes import AttributesMixinSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import (
@@ -14,7 +15,7 @@ from authentik.core.models import (
)
class ApplicationEntitlementSerializer(ModelSerializer):
class ApplicationEntitlementSerializer(AttributesMixinSerializer, ModelSerializer):
"""ApplicationEntitlement Serializer"""
def validate_app(self, app: Application) -> Application:

View File

@@ -30,6 +30,7 @@ from authentik.api.search.fields import (
JSONSearchField,
)
from authentik.api.validation import validate
from authentik.core.api.object_attributes import AttributesMixinSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.core.models import Group, User
@@ -146,7 +147,7 @@ class RelatedGroupSerializer(ModelSerializer):
]
class GroupSerializer(ModelSerializer):
class GroupSerializer(AttributesMixinSerializer, ModelSerializer):
"""Group Serializer"""
attributes = JSONDictField(required=False)

View File

@@ -0,0 +1,94 @@
from typing import Any
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, SerializerMethodField
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import AttributesMixin, ObjectAttribute
from authentik.lib.utils.dict import get_path_from_dict
class AttributesMixinSerializer(ModelSerializer):
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
model = self.Meta.model
attrs = data.get("attributes", {})
attributes = ObjectAttribute.objects.filter(
object_type=ContentType.objects.get_for_model(model),
enabled=True,
)
for attr in attributes:
value = get_path_from_dict(attrs, attr.key)
attr.run_validation(value)
return data
class ContentTypeSerializer(ModelSerializer):
app_label = CharField(read_only=True)
model = CharField(read_only=True)
verbose_name_plural = SerializerMethodField()
fully_qualified_model = SerializerMethodField()
def get_fully_qualified_model(self, ct: ContentType) -> str:
return f"{ct.app_label}.{ct.model}"
def get_verbose_name_plural(self, ct: ContentType) -> str:
return ct.model_class()._meta.verbose_name_plural
class Meta:
model = ContentType
fields = ("id", "app_label", "model", "verbose_name_plural", "fully_qualified_model")
class ObjectAttributeSerializer(ModelSerializer):
object_type = CharField()
object_type_obj = ContentTypeSerializer(read_only=True, source="object_type")
def validate_object_type(self, fqm: str) -> ContentType:
app_label, _, model = fqm.partition(".")
ct = ContentType.objects.filter(app_label=app_label, model=model).first()
if not ct or not issubclass(ct.model_class(), AttributesMixin):
raise ValidationError("Invalid object type")
return ct
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
if attrs.get("is_unique") and attrs.get("is_array"):
raise ValidationError(_("Unique cannot be enabled for arrays."))
return super().validate(attrs)
class Meta:
model = ObjectAttribute
fields = [
"pk",
"object_type",
"object_type_obj",
"enabled",
"created",
"key",
"label",
"last_updated",
"regex",
"type",
"group",
"managed",
"is_unique",
"is_required",
"is_array",
]
extra_kwargs = {
"last_updated": {"read_only": True},
"created": {"read_only": True},
"pk": {"read_only": True},
}
class ObjectAttributeViewSet(ModelViewSet):
serializer_class = ObjectAttributeSerializer
queryset = ObjectAttribute.objects.all()
filterset_fields = ["object_type__model", "object_type__app_label", "enabled"]
search_fields = ["key", "label", "group", "object_type__model", "object_type__app_label"]
ordering = ["key"]

View File

@@ -65,6 +65,7 @@ from authentik.api.search.fields import (
from authentik.api.validation import validate
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand
from authentik.core.api.object_attributes import AttributesMixinSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
JSONDictField,
@@ -134,7 +135,7 @@ class PartialGroupSerializer(ModelSerializer):
]
class UserSerializer(ModelSerializer):
class UserSerializer(AttributesMixinSerializer, ModelSerializer):
"""User Serializer"""
is_superuser = SerializerMethodField()

View File

@@ -0,0 +1,62 @@
# Generated by Django 5.2.13 on 2026-04-11 18:35
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0057_remove_user_groups_remove_user_user_permissions_and_more"),
("contenttypes", "0002_remove_content_type_name"),
]
operations = [
migrations.CreateModel(
name="ObjectAttribute",
fields=[
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
(
"managed",
models.TextField(
default=None,
help_text="Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
null=True,
unique=True,
verbose_name="Managed by authentik",
),
),
(
"attribute_id",
models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
),
("enabled", models.BooleanField(default=True)),
("label", models.TextField()),
("group", models.TextField(blank=True)),
("key", models.TextField()),
(
"type",
models.TextField(
choices=[("text", "Text"), ("number", "Number"), ("boolean", "Boolean")]
),
),
("is_unique", models.BooleanField(default=False)),
("is_required", models.BooleanField(default=False)),
("regex", models.TextField(blank=True)),
("is_array", models.BooleanField(default=False)),
(
"object_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
),
),
],
options={
"verbose_name": "Object Attribute",
"verbose_name_plural": "Object Attributes",
"unique_together": {("object_type", "key", "enabled")},
},
),
]

View File

@@ -13,6 +13,7 @@ from deepmerge import always_merger
from django.contrib.auth.hashers import check_password, identify_hasher
from django.contrib.auth.models import AbstractUser, Permission
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.base_session import AbstractBaseSession
from django.core.validators import validate_slug
from django.db import models
@@ -26,6 +27,7 @@ from guardian.models import RoleModelPermission, RoleObjectPermission
from model_utils.managers import InheritanceManager
from psqlextra.indexes import UniqueIndex
from psqlextra.models import PostgresMaterializedViewModel
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
@@ -1392,3 +1394,61 @@ class AuthenticatedSession(SerializerModel):
session=Session.objects.filter(session_key=request.session.session_key).first(),
user=user,
)
class ObjectAttribute(SerializerModel, ManagedModel, CreatedUpdatedModel):
"""User-defined schema for models' `attributes` JSON field."""
class AttributeType(models.TextChoices):
TEXT = "text"
NUMBER = "number"
BOOLEAN = "boolean"
attribute_id = models.UUIDField(default=uuid4, primary_key=True)
enabled = models.BooleanField(default=True)
object_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
label = models.TextField()
group = models.TextField(blank=True)
key = models.TextField()
type = models.TextField(choices=AttributeType.choices)
is_unique = models.BooleanField(default=False)
is_required = models.BooleanField(default=False)
regex = models.TextField(blank=True)
is_array = models.BooleanField(default=False)
def run_validation(self, value: Any) -> None:
err_key = f"attributes_{self.key.replace(".", "_")}"
if self.is_required and value is None:
raise ValidationError({err_key: _("This field is required")})
if self.is_array:
if not isinstance(value, (list, tuple)):
raise ValidationError({err_key: _("Value must be an array.")})
if self.regex != "":
if not all(re.fullmatch(self.regex, v) for v in value):
raise ValidationError({err_key: _("Value does not match configured pattern.")})
else:
if self.is_unique:
model: type[models.Model] = self.object_type.model_class()
lookup_key = f"attributes__{self.key.replace(".", "__")}"
if model.objects.filter(**{lookup_key: value}).exists():
raise ValidationError({err_key: _("Value is not unique.")})
if self.regex != "":
if not re.fullmatch(self.regex, value):
raise ValidationError({err_key: _("Value does not match configured pattern.")})
@property
def serializer(self) -> type[Serializer]:
from authentik.core.api.object_attributes import ObjectAttributeSerializer
return ObjectAttributeSerializer
def __str__(self):
return f"Object attribute '{self.key}' for content type {self.object_type_id}"
class Meta:
verbose_name = _("Object Attribute")
verbose_name_plural = _("Object Attributes")
unique_together = (("object_type", "key", "enabled"),)

View File

@@ -0,0 +1,198 @@
"""Test object attributes API"""
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.api.object_attributes import ContentType
from authentik.core.models import ObjectAttribute, User
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
class TestObjectAttributesAPI(APITestCase):
"""Test object attributes API"""
def setUp(self) -> None:
super().setUp()
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_create(self):
res = self.client.post(
reverse("authentik_api:objectattribute-list"),
data={
"object_type": "authentik_core.user",
"enabled": False,
"key": "employeeNumber",
"label": "Employee Number",
"type": "text",
"group": "Employee",
"is_unique": False,
"is_required": False,
},
)
self.assertEqual(res.status_code, 201)
attr = ObjectAttribute.objects.filter(key="employeeNumber").first()
self.assertIsNotNone(attr)
def test_create_invalid(self):
res = self.client.post(
reverse("authentik_api:objectattribute-list"),
data={
"object_type": "authentik_core.objectattribute",
"enabled": False,
"key": "employeeNumber",
"label": "Employee Number",
"type": "text",
"group": "Employee",
"is_unique": False,
"is_required": False,
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(res.content, {"object_type": ["Invalid object type"]})
def test_create_invalid_array_unique(self):
res = self.client.post(
reverse("authentik_api:objectattribute-list"),
data={
"object_type": "authentik_core.user",
"enabled": False,
"key": "employeeNumber",
"label": "Employee Number",
"type": "text",
"group": "Employee",
"is_unique": True,
"is_required": False,
"is_array": True,
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(
res.content, {"non_field_errors": ["Unique cannot be enabled for arrays."]}
)
def test_update(self):
attr = ObjectAttribute.objects.create(
object_type=ContentType.objects.get_for_model(User),
label="foo",
key=generate_id(),
type=ObjectAttribute.AttributeType.TEXT,
)
res = self.client.put(
reverse("authentik_api:objectattribute-detail", kwargs={"pk": attr.pk}),
data={
"object_type": "authentik_core.user",
"enabled": False,
"key": attr.key,
"label": "Employee Number",
"type": "text",
"group": "Employee",
"is_unique": False,
"is_required": False,
},
)
self.assertEqual(res.status_code, 200)
attr.refresh_from_db()
self.assertEqual(attr.label, "Employee Number")
def test_user_attrib_validation_required(self):
attr = ObjectAttribute.objects.create(
object_type=ContentType.objects.get_for_model(User),
label="foo",
key=generate_id(),
type=ObjectAttribute.AttributeType.TEXT,
is_required=True,
)
res = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
data={
"attributes": {},
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(res.content, {f"attributes_{attr.key}": ["This field is required"]})
def test_user_attrib_validation_unique(self):
attr = ObjectAttribute.objects.create(
object_type=ContentType.objects.get_for_model(User),
label="foo",
key=generate_id(),
type=ObjectAttribute.AttributeType.TEXT,
is_unique=True,
)
other_user = create_test_user()
other_user.attributes[attr.key] = "foo"
other_user.save()
res = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
data={
"attributes": {attr.key: "foo"},
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(res.content, {f"attributes_{attr.key}": ["Value is not unique."]})
def test_user_attrib_validation_regex(self):
attr = ObjectAttribute.objects.create(
object_type=ContentType.objects.get_for_model(User),
label="foo",
key=generate_id(),
type=ObjectAttribute.AttributeType.TEXT,
regex="bar",
)
res = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
data={
"attributes": {attr.key: "foo"},
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(
res.content, {f"attributes_{attr.key}": ["Value does not match configured pattern."]}
)
def test_user_attrib_validation_array(self):
attr = ObjectAttribute.objects.create(
object_type=ContentType.objects.get_for_model(User),
label="foo",
key=generate_id(),
type=ObjectAttribute.AttributeType.TEXT,
is_array=True,
)
res = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
data={
"attributes": {attr.key: "foo"},
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(res.content, {f"attributes_{attr.key}": ["Value must be an array."]})
res = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
data={
"attributes": {attr.key: ["foo"]},
},
)
self.assertEqual(res.status_code, 200)
def test_user_attrib_validation_array_regex(self):
attr = ObjectAttribute.objects.create(
object_type=ContentType.objects.get_for_model(User),
label="foo",
key=generate_id(),
type=ObjectAttribute.AttributeType.TEXT,
is_array=True,
regex="bar",
)
res = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": self.user.pk}),
data={
"attributes": {attr.key: ["foo"]},
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(
res.content, {f"attributes_{attr.key}": ["Value does not match configured pattern."]}
)

View File

@@ -8,6 +8,7 @@ from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.object_attributes import ObjectAttributeViewSet
from authentik.core.api.property_mappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet
from authentik.core.api.sources import (
@@ -87,6 +88,7 @@ api_urlpatterns = [
("core/groups", GroupViewSet),
("core/users", UserViewSet),
("core/tokens", TokenViewSet),
("core/object_attributes", ObjectAttributeViewSet),
("sources/all", SourceViewSet),
("sources/user_connections/all", UserSourceConnectionViewSet),
("sources/group_connections/all", GroupSourceConnectionViewSet),

View File

@@ -1,11 +1,12 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.object_attributes import AttributesMixinSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.models import DeviceAccessGroup
class DeviceAccessGroupSerializer(ModelSerializer):
class DeviceAccessGroupSerializer(AttributesMixinSerializer, ModelSerializer):
class Meta:
model = DeviceAccessGroup

View File

@@ -4,13 +4,13 @@ from django.urls import reverse
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import CharField, SerializerMethodField
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.object_attributes import ContentTypeSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.reports.models import DataExport
@@ -18,19 +18,6 @@ from authentik.enterprise.reports.tasks import generate_export
from authentik.rbac.permissions import HasPermission
class ContentTypeSerializer(ModelSerializer):
app_label = CharField(read_only=True)
model = CharField(read_only=True)
verbose_name_plural = SerializerMethodField()
def get_verbose_name_plural(self, ct: ContentType) -> str:
return ct.model_class()._meta.verbose_name_plural
class Meta:
model = ContentType
fields = ("id", "app_label", "model", "verbose_name_plural")
class DataExportSerializer(EnterpriseRequiredMixin, ModelSerializer):
requested_by = PartialUserSerializer(read_only=True)
content_type = ContentTypeSerializer(read_only=True)

View File

@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError
from django.db.models import Model
from django.http import HttpRequest
from django.utils.text import slugify
from django.utils.timezone import now
@@ -22,6 +23,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.events.models import Event
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.utils.dict import get_path_from_dict
from authentik.lib.utils.email import normalize_addresses
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.time import timedelta_from_string
@@ -70,6 +72,7 @@ class BaseEvaluator:
"ak_send_email": self.expr_send_email,
"ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
"ak_obj_attr": BaseEvaluator.expr_obj_attr,
"ip_address": ip_address,
"ip_network": ip_network,
"list_flatten": BaseEvaluator.expr_flatten,
@@ -162,6 +165,16 @@ class BaseEvaluator:
return False
return len(list(user_devices)) > 0
@staticmethod
def expr_obj_attr(obj: Model, attr_key: str, fallback: str) -> Any:
"""Get an attribute of the given object if set by its dotted path, otherwise
return fallback value."""
attrs = getattr(obj, "attributes", {})
value = get_path_from_dict(attrs, attr_key)
if value is None and fallback:
return getattr(obj, fallback)
return value
def expr_event_create(self, action: str, **kwargs):
"""Create event with supplied data and try to extract as much relevant data
from the context"""

View File

@@ -296,6 +296,46 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_core.objectattribute"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"created",
"must_created",
"present"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_core.objectattribute_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_core.objectattribute"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_core.objectattribute"
}
}
},
{
"type": "object",
"required": [
@@ -5420,6 +5460,95 @@
}
}
},
"model_authentik_core.objectattribute": {
"type": "object",
"properties": {
"object_type": {
"type": "string",
"minLength": 1,
"title": "Object type"
},
"enabled": {
"type": "boolean",
"title": "Enabled"
},
"key": {
"type": "string",
"minLength": 1,
"title": "Key"
},
"label": {
"type": "string",
"minLength": 1,
"title": "Label"
},
"regex": {
"type": "string",
"title": "Regex"
},
"type": {
"type": "string",
"enum": [
"text",
"number",
"boolean"
],
"title": "Type"
},
"group": {
"type": "string",
"title": "Group"
},
"managed": {
"type": [
"string",
"null"
],
"minLength": 1,
"title": "Managed by authentik",
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
},
"is_unique": {
"type": "boolean",
"title": "Is unique"
},
"is_required": {
"type": "boolean",
"title": "Is required"
},
"is_array": {
"type": "boolean",
"title": "Is array"
}
},
"required": []
},
"model_authentik_core.objectattribute_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_objectattribute",
"change_objectattribute",
"delete_objectattribute",
"view_objectattribute"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_core.token": {
"type": "object",
"properties": {
@@ -5610,6 +5739,7 @@
"authentik_core.add_groupancestrynode",
"authentik_core.add_groupparentagenode",
"authentik_core.add_groupsourceconnection",
"authentik_core.add_objectattribute",
"authentik_core.add_propertymapping",
"authentik_core.add_provider",
"authentik_core.add_source",
@@ -5624,6 +5754,7 @@
"authentik_core.change_groupancestrynode",
"authentik_core.change_groupparentagenode",
"authentik_core.change_groupsourceconnection",
"authentik_core.change_objectattribute",
"authentik_core.change_propertymapping",
"authentik_core.change_provider",
"authentik_core.change_source",
@@ -5637,6 +5768,7 @@
"authentik_core.delete_groupancestrynode",
"authentik_core.delete_groupparentagenode",
"authentik_core.delete_groupsourceconnection",
"authentik_core.delete_objectattribute",
"authentik_core.delete_propertymapping",
"authentik_core.delete_provider",
"authentik_core.delete_source",
@@ -5657,6 +5789,7 @@
"authentik_core.view_groupancestrynode",
"authentik_core.view_groupparentagenode",
"authentik_core.view_groupsourceconnection",
"authentik_core.view_objectattribute",
"authentik_core.view_propertymapping",
"authentik_core.view_provider",
"authentik_core.view_source",
@@ -9085,6 +9218,7 @@
"authentik_core.application",
"authentik_core.applicationentitlement",
"authentik_core.token",
"authentik_core.objectattribute",
"authentik_crypto.certificatekeypair",
"authentik_endpoints.deviceuserbinding",
"authentik_endpoints.deviceaccessgroup",
@@ -11376,6 +11510,7 @@
"authentik_core.add_groupancestrynode",
"authentik_core.add_groupparentagenode",
"authentik_core.add_groupsourceconnection",
"authentik_core.add_objectattribute",
"authentik_core.add_propertymapping",
"authentik_core.add_provider",
"authentik_core.add_source",
@@ -11390,6 +11525,7 @@
"authentik_core.change_groupancestrynode",
"authentik_core.change_groupparentagenode",
"authentik_core.change_groupsourceconnection",
"authentik_core.change_objectattribute",
"authentik_core.change_propertymapping",
"authentik_core.change_provider",
"authentik_core.change_source",
@@ -11403,6 +11539,7 @@
"authentik_core.delete_groupancestrynode",
"authentik_core.delete_groupparentagenode",
"authentik_core.delete_groupsourceconnection",
"authentik_core.delete_objectattribute",
"authentik_core.delete_propertymapping",
"authentik_core.delete_provider",
"authentik_core.delete_source",
@@ -11423,6 +11560,7 @@
"authentik_core.view_groupancestrynode",
"authentik_core.view_groupparentagenode",
"authentik_core.view_groupsourceconnection",
"authentik_core.view_objectattribute",
"authentik_core.view_propertymapping",
"authentik_core.view_provider",
"authentik_core.view_source",

View File

@@ -0,0 +1,145 @@
version: 1
metadata:
labels:
blueprints.goauthentik.io/system: "true"
name: System - Object Attributes - User
entries:
identity:
- model: authentik_core.objectattribute
identifiers:
key: givenName
managed: goauthentik.io/object-attrs/user/identity/givenName
attrs:
group: Identity
object_type: authentik_core.user
label: Given Name
enabled: false
is_required: false
is_unique: false
type: text
- model: authentik_core.objectattribute
identifiers:
key: familyName
managed: goauthentik.io/object-attrs/user/identity/familyName
attrs:
group: Identity
object_type: authentik_core.user
label: Family Name
enabled: false
is_required: false
is_unique: false
type: text
ak:
- model: authentik_core.objectattribute
identifiers:
key: settings.locale
managed: goauthentik.io/object-attrs/user/settings/locale
attrs:
group: Settings
object_type: authentik_core.user
label: Locale
enabled: true
is_required: false
is_unique: false
type: text
address:
- model: authentik_core.objectattribute
identifiers:
key: street
managed: goauthentik.io/object-attrs/user/address/street
attrs:
group: Address
object_type: authentik_core.user
label: Street
enabled: false
is_required: false
is_unique: false
type: text
- model: authentik_core.objectattribute
identifiers:
key: state
managed: goauthentik.io/object-attrs/user/address/state
attrs:
group: Address
object_type: authentik_core.user
label: State
enabled: false
is_required: false
is_unique: false
type: text
- model: authentik_core.objectattribute
identifiers:
key: location
managed: goauthentik.io/object-attrs/user/address/location
attrs:
group: Address
object_type: authentik_core.user
label: Location
enabled: false
is_required: false
is_unique: false
type: text
- model: authentik_core.objectattribute
identifiers:
key: postalCode
managed: goauthentik.io/object-attrs/user/address/postal-code
attrs:
group: Address
object_type: authentik_core.user
label: Postal code
enabled: false
is_required: false
is_unique: false
type: text
contact:
- model: authentik_core.objectattribute
identifiers:
key: phoneNumber
managed: goauthentik.io/object-attrs/user/contact/phone-number
attrs:
group: Contact
object_type: authentik_core.user
label: Phone number(s)
enabled: false
is_required: false
is_unique: false
type: text
is_array: true
unix:
- model: authentik_core.objectattribute
identifiers:
key: shell
managed: goauthentik.io/object-attrs/user/unix/shell
attrs:
group: Unix
object_type: authentik_core.user
label: Shell
enabled: false
is_required: false
is_unique: false
type: text
employee:
- model: authentik_core.objectattribute
identifiers:
key: employeeNumber
managed: goauthentik.io/object-attrs/user/employee/number
attrs:
group: Employee
object_type: authentik_core.user
label: Employee Number
enabled: false
is_required: false
is_unique: false
type: text
- model: authentik_core.objectattribute
identifiers:
key: jobTitle
managed: goauthentik.io/object-attrs/user/employee/title
attrs:
group: Employee
object_type: authentik_core.user
label: Title
enabled: false
is_required: false
is_unique: false
type: text

View File

@@ -35,14 +35,13 @@ entries:
description: "General Profile Information"
expression: |
return {
# Because authentik only saves the user's full name, and has no concept of first and last names,
# the full name is used as given name.
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
"name": request.user.name,
"given_name": request.user.name,
"given_name": ak_obj_attr(request.user, "givenName", "name"),
"family_name": ak_obj_attr(request.user, "familyName"),
"preferred_username": request.user.username,
"nickname": request.user.username,
"groups": [group.name for group in request.user.groups.all()],
"picture": request.user.avatar,
}
- identifiers:
managed: goauthentik.io/providers/oauth2/scope-entitlements

View File

@@ -42,8 +42,8 @@ entries:
"userName": request.user.username,
"name": {
"formatted": formatted,
"givenName": givenName,
"familyName": familyName,
"givenName": ak_obj_attr(request.user, "givenName") or givenName,
"familyName": ak_obj_attr(request.user, "familyName") or familyName,
},
"displayName": request.user.name,
"photos": photos,

View File

@@ -58,6 +58,10 @@ export interface AdminFileUsedByListRequest {
name?: string;
}
export interface AdminModelsListRequest {
filterHasAttributes?: boolean | null;
}
export interface AdminSettingsPartialUpdateRequest {
patchedSettingsRequest?: PatchedSettingsRequest;
}
@@ -400,9 +404,15 @@ export class AdminApi extends runtime.BaseAPI {
/**
* Creates request options for adminModelsList without sending the request
*/
async adminModelsListRequestOpts(): Promise<runtime.RequestOpts> {
async adminModelsListRequestOpts(
requestParameters: AdminModelsListRequest,
): Promise<runtime.RequestOpts> {
const queryParameters: any = {};
if (requestParameters["filterHasAttributes"] != null) {
queryParameters["filter_has_attributes"] = requestParameters["filterHasAttributes"];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
@@ -428,9 +438,10 @@ export class AdminApi extends runtime.BaseAPI {
* Read-only view list all installed models
*/
async adminModelsListRaw(
requestParameters: AdminModelsListRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<Array<App>>> {
const requestOptions = await this.adminModelsListRequestOpts();
const requestOptions = await this.adminModelsListRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(AppFromJSON));
@@ -440,9 +451,10 @@ export class AdminApi extends runtime.BaseAPI {
* Read-only view list all installed models
*/
async adminModelsList(
requestParameters: AdminModelsListRequest = {},
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<Array<App>> {
const response = await this.adminModelsListRaw(initOverrides);
const response = await this.adminModelsListRaw(requestParameters, initOverrides);
return await response.value();
}

View File

@@ -28,11 +28,14 @@ import type {
ImpersonationRequest,
IntentEnum,
Link,
ObjectAttribute,
ObjectAttributeRequest,
PaginatedApplicationEntitlementList,
PaginatedApplicationList,
PaginatedAuthenticatedSessionList,
PaginatedBrandList,
PaginatedGroupList,
PaginatedObjectAttributeList,
PaginatedTokenList,
PaginatedUserConsentList,
PaginatedUserList,
@@ -40,6 +43,7 @@ import type {
PatchedApplicationRequest,
PatchedBrandRequest,
PatchedGroupRequest,
PatchedObjectAttributeRequest,
PatchedTokenRequest,
PatchedUserRequest,
PolicyTestResult,
@@ -80,11 +84,14 @@ import {
GroupRequestToJSON,
ImpersonationRequestToJSON,
LinkFromJSON,
ObjectAttributeFromJSON,
ObjectAttributeRequestToJSON,
PaginatedApplicationEntitlementListFromJSON,
PaginatedApplicationListFromJSON,
PaginatedAuthenticatedSessionListFromJSON,
PaginatedBrandListFromJSON,
PaginatedGroupListFromJSON,
PaginatedObjectAttributeListFromJSON,
PaginatedTokenListFromJSON,
PaginatedUserConsentListFromJSON,
PaginatedUserListFromJSON,
@@ -92,6 +99,7 @@ import {
PatchedApplicationRequestToJSON,
PatchedBrandRequestToJSON,
PatchedGroupRequestToJSON,
PatchedObjectAttributeRequestToJSON,
PatchedTokenRequestToJSON,
PatchedUserRequestToJSON,
PolicyTestResultFromJSON,
@@ -332,6 +340,38 @@ export interface CoreGroupsUsedByListRequest {
groupUuid: string;
}
export interface CoreObjectAttributesCreateRequest {
objectAttributeRequest: ObjectAttributeRequest;
}
export interface CoreObjectAttributesDestroyRequest {
attributeId: string;
}
export interface CoreObjectAttributesListRequest {
enabled?: boolean;
objectTypeAppLabel?: string;
objectTypeModel?: string;
ordering?: string;
page?: number;
pageSize?: number;
search?: string;
}
export interface CoreObjectAttributesPartialUpdateRequest {
attributeId: string;
patchedObjectAttributeRequest?: PatchedObjectAttributeRequest;
}
export interface CoreObjectAttributesRetrieveRequest {
attributeId: string;
}
export interface CoreObjectAttributesUpdateRequest {
attributeId: string;
objectAttributeRequest: ObjectAttributeRequest;
}
export interface CoreTokensCreateRequest {
tokenRequest: TokenRequest;
}
@@ -3237,6 +3277,426 @@ export class CoreApi extends runtime.BaseAPI {
return await response.value();
}
/**
* Creates request options for coreObjectAttributesCreate without sending the request
*/
async coreObjectAttributesCreateRequestOpts(
requestParameters: CoreObjectAttributesCreateRequest,
): Promise<runtime.RequestOpts> {
if (requestParameters["objectAttributeRequest"] == null) {
throw new runtime.RequiredError(
"objectAttributeRequest",
'Required parameter "objectAttributeRequest" was null or undefined when calling coreObjectAttributesCreate().',
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters["Content-Type"] = "application/json";
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/core/object_attributes/`;
return {
path: urlPath,
method: "POST",
headers: headerParameters,
query: queryParameters,
body: ObjectAttributeRequestToJSON(requestParameters["objectAttributeRequest"]),
};
}
/**
*/
async coreObjectAttributesCreateRaw(
requestParameters: CoreObjectAttributesCreateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<ObjectAttribute>> {
const requestOptions = await this.coreObjectAttributesCreateRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) =>
ObjectAttributeFromJSON(jsonValue),
);
}
/**
*/
async coreObjectAttributesCreate(
requestParameters: CoreObjectAttributesCreateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<ObjectAttribute> {
const response = await this.coreObjectAttributesCreateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for coreObjectAttributesDestroy without sending the request
*/
async coreObjectAttributesDestroyRequestOpts(
requestParameters: CoreObjectAttributesDestroyRequest,
): Promise<runtime.RequestOpts> {
if (requestParameters["attributeId"] == null) {
throw new runtime.RequiredError(
"attributeId",
'Required parameter "attributeId" was null or undefined when calling coreObjectAttributesDestroy().',
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/core/object_attributes/{attribute_id}/`;
urlPath = urlPath.replace(
`{${"attribute_id"}}`,
encodeURIComponent(String(requestParameters["attributeId"])),
);
return {
path: urlPath,
method: "DELETE",
headers: headerParameters,
query: queryParameters,
};
}
/**
*/
async coreObjectAttributesDestroyRaw(
requestParameters: CoreObjectAttributesDestroyRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<void>> {
const requestOptions = await this.coreObjectAttributesDestroyRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
*/
async coreObjectAttributesDestroy(
requestParameters: CoreObjectAttributesDestroyRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<void> {
await this.coreObjectAttributesDestroyRaw(requestParameters, initOverrides);
}
/**
* Creates request options for coreObjectAttributesList without sending the request
*/
async coreObjectAttributesListRequestOpts(
requestParameters: CoreObjectAttributesListRequest,
): Promise<runtime.RequestOpts> {
const queryParameters: any = {};
if (requestParameters["enabled"] != null) {
queryParameters["enabled"] = requestParameters["enabled"];
}
if (requestParameters["objectTypeAppLabel"] != null) {
queryParameters["object_type__app_label"] = requestParameters["objectTypeAppLabel"];
}
if (requestParameters["objectTypeModel"] != null) {
queryParameters["object_type__model"] = requestParameters["objectTypeModel"];
}
if (requestParameters["ordering"] != null) {
queryParameters["ordering"] = requestParameters["ordering"];
}
if (requestParameters["page"] != null) {
queryParameters["page"] = requestParameters["page"];
}
if (requestParameters["pageSize"] != null) {
queryParameters["page_size"] = requestParameters["pageSize"];
}
if (requestParameters["search"] != null) {
queryParameters["search"] = requestParameters["search"];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/core/object_attributes/`;
return {
path: urlPath,
method: "GET",
headers: headerParameters,
query: queryParameters,
};
}
/**
*/
async coreObjectAttributesListRaw(
requestParameters: CoreObjectAttributesListRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<PaginatedObjectAttributeList>> {
const requestOptions = await this.coreObjectAttributesListRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) =>
PaginatedObjectAttributeListFromJSON(jsonValue),
);
}
/**
*/
async coreObjectAttributesList(
requestParameters: CoreObjectAttributesListRequest = {},
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<PaginatedObjectAttributeList> {
const response = await this.coreObjectAttributesListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for coreObjectAttributesPartialUpdate without sending the request
*/
async coreObjectAttributesPartialUpdateRequestOpts(
requestParameters: CoreObjectAttributesPartialUpdateRequest,
): Promise<runtime.RequestOpts> {
if (requestParameters["attributeId"] == null) {
throw new runtime.RequiredError(
"attributeId",
'Required parameter "attributeId" was null or undefined when calling coreObjectAttributesPartialUpdate().',
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters["Content-Type"] = "application/json";
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/core/object_attributes/{attribute_id}/`;
urlPath = urlPath.replace(
`{${"attribute_id"}}`,
encodeURIComponent(String(requestParameters["attributeId"])),
);
return {
path: urlPath,
method: "PATCH",
headers: headerParameters,
query: queryParameters,
body: PatchedObjectAttributeRequestToJSON(
requestParameters["patchedObjectAttributeRequest"],
),
};
}
/**
*/
async coreObjectAttributesPartialUpdateRaw(
requestParameters: CoreObjectAttributesPartialUpdateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<ObjectAttribute>> {
const requestOptions =
await this.coreObjectAttributesPartialUpdateRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) =>
ObjectAttributeFromJSON(jsonValue),
);
}
/**
*/
async coreObjectAttributesPartialUpdate(
requestParameters: CoreObjectAttributesPartialUpdateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<ObjectAttribute> {
const response = await this.coreObjectAttributesPartialUpdateRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Creates request options for coreObjectAttributesRetrieve without sending the request
*/
async coreObjectAttributesRetrieveRequestOpts(
requestParameters: CoreObjectAttributesRetrieveRequest,
): Promise<runtime.RequestOpts> {
if (requestParameters["attributeId"] == null) {
throw new runtime.RequiredError(
"attributeId",
'Required parameter "attributeId" was null or undefined when calling coreObjectAttributesRetrieve().',
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/core/object_attributes/{attribute_id}/`;
urlPath = urlPath.replace(
`{${"attribute_id"}}`,
encodeURIComponent(String(requestParameters["attributeId"])),
);
return {
path: urlPath,
method: "GET",
headers: headerParameters,
query: queryParameters,
};
}
/**
*/
async coreObjectAttributesRetrieveRaw(
requestParameters: CoreObjectAttributesRetrieveRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<ObjectAttribute>> {
const requestOptions =
await this.coreObjectAttributesRetrieveRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) =>
ObjectAttributeFromJSON(jsonValue),
);
}
/**
*/
async coreObjectAttributesRetrieve(
requestParameters: CoreObjectAttributesRetrieveRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<ObjectAttribute> {
const response = await this.coreObjectAttributesRetrieveRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/**
* Creates request options for coreObjectAttributesUpdate without sending the request
*/
async coreObjectAttributesUpdateRequestOpts(
requestParameters: CoreObjectAttributesUpdateRequest,
): Promise<runtime.RequestOpts> {
if (requestParameters["attributeId"] == null) {
throw new runtime.RequiredError(
"attributeId",
'Required parameter "attributeId" was null or undefined when calling coreObjectAttributesUpdate().',
);
}
if (requestParameters["objectAttributeRequest"] == null) {
throw new runtime.RequiredError(
"objectAttributeRequest",
'Required parameter "objectAttributeRequest" was null or undefined when calling coreObjectAttributesUpdate().',
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters["Content-Type"] = "application/json";
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/core/object_attributes/{attribute_id}/`;
urlPath = urlPath.replace(
`{${"attribute_id"}}`,
encodeURIComponent(String(requestParameters["attributeId"])),
);
return {
path: urlPath,
method: "PUT",
headers: headerParameters,
query: queryParameters,
body: ObjectAttributeRequestToJSON(requestParameters["objectAttributeRequest"]),
};
}
/**
*/
async coreObjectAttributesUpdateRaw(
requestParameters: CoreObjectAttributesUpdateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<ObjectAttribute>> {
const requestOptions = await this.coreObjectAttributesUpdateRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) =>
ObjectAttributeFromJSON(jsonValue),
);
}
/**
*/
async coreObjectAttributesUpdate(
requestParameters: CoreObjectAttributesUpdateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<ObjectAttribute> {
const response = await this.coreObjectAttributesUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Creates request options for coreTokensCreate without sending the request
*/

View File

@@ -42,6 +42,12 @@ export interface ContentType {
* @memberof ContentType
*/
readonly verboseNamePlural: string;
/**
*
* @type {string}
* @memberof ContentType
*/
readonly fullyQualifiedModel: string;
}
/**
@@ -52,6 +58,8 @@ export function instanceOfContentType(value: object): value is ContentType {
if (!("appLabel" in value) || value["appLabel"] === undefined) return false;
if (!("model" in value) || value["model"] === undefined) return false;
if (!("verboseNamePlural" in value) || value["verboseNamePlural"] === undefined) return false;
if (!("fullyQualifiedModel" in value) || value["fullyQualifiedModel"] === undefined)
return false;
return true;
}
@@ -68,6 +76,7 @@ export function ContentTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean
appLabel: json["app_label"],
model: json["model"],
verboseNamePlural: json["verbose_name_plural"],
fullyQualifiedModel: json["fully_qualified_model"],
};
}
@@ -76,7 +85,10 @@ export function ContentTypeToJSON(json: any): ContentType {
}
export function ContentTypeToJSONTyped(
value?: Omit<ContentType, "id" | "app_label" | "model" | "verbose_name_plural"> | null,
value?: Omit<
ContentType,
"id" | "app_label" | "model" | "verbose_name_plural" | "fully_qualified_model"
> | null,
ignoreDiscriminator: boolean = false,
): any {
if (value == null) {

View File

@@ -23,6 +23,7 @@ export const ModelEnum = {
AuthentikCoreApplication: "authentik_core.application",
AuthentikCoreApplicationentitlement: "authentik_core.applicationentitlement",
AuthentikCoreToken: "authentik_core.token",
AuthentikCoreObjectattribute: "authentik_core.objectattribute",
AuthentikCryptoCertificatekeypair: "authentik_crypto.certificatekeypair",
AuthentikEndpointsDeviceuserbinding: "authentik_endpoints.deviceuserbinding",
AuthentikEndpointsDeviceaccessgroup: "authentik_endpoints.deviceaccessgroup",

View File

@@ -0,0 +1,191 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { ContentType } from "./ContentType";
import { ContentTypeFromJSON } from "./ContentType";
import type { ObjectAttributeTypeEnum } from "./ObjectAttributeTypeEnum";
import {
ObjectAttributeTypeEnumFromJSON,
ObjectAttributeTypeEnumToJSON,
} from "./ObjectAttributeTypeEnum";
/**
*
* @export
* @interface ObjectAttribute
*/
export interface ObjectAttribute {
/**
*
* @type {string}
* @memberof ObjectAttribute
*/
readonly pk: string;
/**
*
* @type {string}
* @memberof ObjectAttribute
*/
objectType: string;
/**
*
* @type {ContentType}
* @memberof ObjectAttribute
*/
readonly objectTypeObj: ContentType;
/**
*
* @type {boolean}
* @memberof ObjectAttribute
*/
enabled?: boolean;
/**
*
* @type {Date}
* @memberof ObjectAttribute
*/
readonly created: Date;
/**
*
* @type {string}
* @memberof ObjectAttribute
*/
key: string;
/**
*
* @type {string}
* @memberof ObjectAttribute
*/
label: string;
/**
*
* @type {Date}
* @memberof ObjectAttribute
*/
readonly lastUpdated: Date;
/**
*
* @type {string}
* @memberof ObjectAttribute
*/
regex?: string;
/**
*
* @type {ObjectAttributeTypeEnum}
* @memberof ObjectAttribute
*/
type: ObjectAttributeTypeEnum;
/**
*
* @type {string}
* @memberof ObjectAttribute
*/
group?: string;
/**
* Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.
* @type {string}
* @memberof ObjectAttribute
*/
managed?: string | null;
/**
*
* @type {boolean}
* @memberof ObjectAttribute
*/
isUnique?: boolean;
/**
*
* @type {boolean}
* @memberof ObjectAttribute
*/
isRequired?: boolean;
/**
*
* @type {boolean}
* @memberof ObjectAttribute
*/
isArray?: boolean;
}
/**
* Check if a given object implements the ObjectAttribute interface.
*/
export function instanceOfObjectAttribute(value: object): value is ObjectAttribute {
if (!("pk" in value) || value["pk"] === undefined) return false;
if (!("objectType" in value) || value["objectType"] === undefined) return false;
if (!("objectTypeObj" in value) || value["objectTypeObj"] === undefined) return false;
if (!("created" in value) || value["created"] === undefined) return false;
if (!("key" in value) || value["key"] === undefined) return false;
if (!("label" in value) || value["label"] === undefined) return false;
if (!("lastUpdated" in value) || value["lastUpdated"] === undefined) return false;
if (!("type" in value) || value["type"] === undefined) return false;
return true;
}
export function ObjectAttributeFromJSON(json: any): ObjectAttribute {
return ObjectAttributeFromJSONTyped(json, false);
}
export function ObjectAttributeFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): ObjectAttribute {
if (json == null) {
return json;
}
return {
pk: json["pk"],
objectType: json["object_type"],
objectTypeObj: ContentTypeFromJSON(json["object_type_obj"]),
enabled: json["enabled"] == null ? undefined : json["enabled"],
created: new Date(json["created"]),
key: json["key"],
label: json["label"],
lastUpdated: new Date(json["last_updated"]),
regex: json["regex"] == null ? undefined : json["regex"],
type: ObjectAttributeTypeEnumFromJSON(json["type"]),
group: json["group"] == null ? undefined : json["group"],
managed: json["managed"] == null ? undefined : json["managed"],
isUnique: json["is_unique"] == null ? undefined : json["is_unique"],
isRequired: json["is_required"] == null ? undefined : json["is_required"],
isArray: json["is_array"] == null ? undefined : json["is_array"],
};
}
export function ObjectAttributeToJSON(json: any): ObjectAttribute {
return ObjectAttributeToJSONTyped(json, false);
}
export function ObjectAttributeToJSONTyped(
value?: Omit<ObjectAttribute, "pk" | "object_type_obj" | "created" | "last_updated"> | null,
ignoreDiscriminator: boolean = false,
): any {
if (value == null) {
return value;
}
return {
object_type: value["objectType"],
enabled: value["enabled"],
key: value["key"],
label: value["label"],
regex: value["regex"],
type: ObjectAttributeTypeEnumToJSON(value["type"]),
group: value["group"],
managed: value["managed"],
is_unique: value["isUnique"],
is_required: value["isRequired"],
is_array: value["isArray"],
};
}

View File

@@ -0,0 +1,157 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { ObjectAttributeTypeEnum } from "./ObjectAttributeTypeEnum";
import {
ObjectAttributeTypeEnumFromJSON,
ObjectAttributeTypeEnumToJSON,
} from "./ObjectAttributeTypeEnum";
/**
*
* @export
* @interface ObjectAttributeRequest
*/
export interface ObjectAttributeRequest {
/**
*
* @type {string}
* @memberof ObjectAttributeRequest
*/
objectType: string;
/**
*
* @type {boolean}
* @memberof ObjectAttributeRequest
*/
enabled?: boolean;
/**
*
* @type {string}
* @memberof ObjectAttributeRequest
*/
key: string;
/**
*
* @type {string}
* @memberof ObjectAttributeRequest
*/
label: string;
/**
*
* @type {string}
* @memberof ObjectAttributeRequest
*/
regex?: string;
/**
*
* @type {ObjectAttributeTypeEnum}
* @memberof ObjectAttributeRequest
*/
type: ObjectAttributeTypeEnum;
/**
*
* @type {string}
* @memberof ObjectAttributeRequest
*/
group?: string;
/**
* Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.
* @type {string}
* @memberof ObjectAttributeRequest
*/
managed?: string | null;
/**
*
* @type {boolean}
* @memberof ObjectAttributeRequest
*/
isUnique?: boolean;
/**
*
* @type {boolean}
* @memberof ObjectAttributeRequest
*/
isRequired?: boolean;
/**
*
* @type {boolean}
* @memberof ObjectAttributeRequest
*/
isArray?: boolean;
}
/**
* Check if a given object implements the ObjectAttributeRequest interface.
*/
export function instanceOfObjectAttributeRequest(value: object): value is ObjectAttributeRequest {
if (!("objectType" in value) || value["objectType"] === undefined) return false;
if (!("key" in value) || value["key"] === undefined) return false;
if (!("label" in value) || value["label"] === undefined) return false;
if (!("type" in value) || value["type"] === undefined) return false;
return true;
}
export function ObjectAttributeRequestFromJSON(json: any): ObjectAttributeRequest {
return ObjectAttributeRequestFromJSONTyped(json, false);
}
export function ObjectAttributeRequestFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): ObjectAttributeRequest {
if (json == null) {
return json;
}
return {
objectType: json["object_type"],
enabled: json["enabled"] == null ? undefined : json["enabled"],
key: json["key"],
label: json["label"],
regex: json["regex"] == null ? undefined : json["regex"],
type: ObjectAttributeTypeEnumFromJSON(json["type"]),
group: json["group"] == null ? undefined : json["group"],
managed: json["managed"] == null ? undefined : json["managed"],
isUnique: json["is_unique"] == null ? undefined : json["is_unique"],
isRequired: json["is_required"] == null ? undefined : json["is_required"],
isArray: json["is_array"] == null ? undefined : json["is_array"],
};
}
export function ObjectAttributeRequestToJSON(json: any): ObjectAttributeRequest {
return ObjectAttributeRequestToJSONTyped(json, false);
}
export function ObjectAttributeRequestToJSONTyped(
value?: ObjectAttributeRequest | null,
ignoreDiscriminator: boolean = false,
): any {
if (value == null) {
return value;
}
return {
object_type: value["objectType"],
enabled: value["enabled"],
key: value["key"],
label: value["label"],
regex: value["regex"],
type: ObjectAttributeTypeEnumToJSON(value["type"]),
group: value["group"],
managed: value["managed"],
is_unique: value["isUnique"],
is_required: value["isRequired"],
is_array: value["isArray"],
};
}

View File

@@ -0,0 +1,59 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const ObjectAttributeTypeEnum = {
Text: "text",
Number: "number",
Boolean: "boolean",
UnknownDefaultOpenApi: "11184809",
} as const;
export type ObjectAttributeTypeEnum =
(typeof ObjectAttributeTypeEnum)[keyof typeof ObjectAttributeTypeEnum];
export function instanceOfObjectAttributeTypeEnum(value: any): boolean {
for (const key in ObjectAttributeTypeEnum) {
if (Object.prototype.hasOwnProperty.call(ObjectAttributeTypeEnum, key)) {
if (ObjectAttributeTypeEnum[key as keyof typeof ObjectAttributeTypeEnum] === value) {
return true;
}
}
}
return false;
}
export function ObjectAttributeTypeEnumFromJSON(json: any): ObjectAttributeTypeEnum {
return ObjectAttributeTypeEnumFromJSONTyped(json, false);
}
export function ObjectAttributeTypeEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): ObjectAttributeTypeEnum {
return json as ObjectAttributeTypeEnum;
}
export function ObjectAttributeTypeEnumToJSON(value?: ObjectAttributeTypeEnum | null): any {
return value as any;
}
export function ObjectAttributeTypeEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): ObjectAttributeTypeEnum {
return value as ObjectAttributeTypeEnum;
}

View File

@@ -0,0 +1,93 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { ObjectAttribute } from "./ObjectAttribute";
import { ObjectAttributeFromJSON, ObjectAttributeToJSON } from "./ObjectAttribute";
import type { Pagination } from "./Pagination";
import { PaginationFromJSON, PaginationToJSON } from "./Pagination";
/**
*
* @export
* @interface PaginatedObjectAttributeList
*/
export interface PaginatedObjectAttributeList {
/**
*
* @type {Pagination}
* @memberof PaginatedObjectAttributeList
*/
pagination: Pagination;
/**
*
* @type {Array<ObjectAttribute>}
* @memberof PaginatedObjectAttributeList
*/
results: Array<ObjectAttribute>;
/**
*
* @type {{ [key: string]: any; }}
* @memberof PaginatedObjectAttributeList
*/
autocomplete: { [key: string]: any };
}
/**
* Check if a given object implements the PaginatedObjectAttributeList interface.
*/
export function instanceOfPaginatedObjectAttributeList(
value: object,
): value is PaginatedObjectAttributeList {
if (!("pagination" in value) || value["pagination"] === undefined) return false;
if (!("results" in value) || value["results"] === undefined) return false;
if (!("autocomplete" in value) || value["autocomplete"] === undefined) return false;
return true;
}
export function PaginatedObjectAttributeListFromJSON(json: any): PaginatedObjectAttributeList {
return PaginatedObjectAttributeListFromJSONTyped(json, false);
}
export function PaginatedObjectAttributeListFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): PaginatedObjectAttributeList {
if (json == null) {
return json;
}
return {
pagination: PaginationFromJSON(json["pagination"]),
results: (json["results"] as Array<any>).map(ObjectAttributeFromJSON),
autocomplete: json["autocomplete"],
};
}
export function PaginatedObjectAttributeListToJSON(json: any): PaginatedObjectAttributeList {
return PaginatedObjectAttributeListToJSONTyped(json, false);
}
export function PaginatedObjectAttributeListToJSONTyped(
value?: PaginatedObjectAttributeList | null,
ignoreDiscriminator: boolean = false,
): any {
if (value == null) {
return value;
}
return {
pagination: PaginationToJSON(value["pagination"]),
results: (value["results"] as Array<any>).map(ObjectAttributeToJSON),
autocomplete: value["autocomplete"],
};
}

View File

@@ -0,0 +1,155 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { ObjectAttributeTypeEnum } from "./ObjectAttributeTypeEnum";
import {
ObjectAttributeTypeEnumFromJSON,
ObjectAttributeTypeEnumToJSON,
} from "./ObjectAttributeTypeEnum";
/**
*
* @export
* @interface PatchedObjectAttributeRequest
*/
export interface PatchedObjectAttributeRequest {
/**
*
* @type {string}
* @memberof PatchedObjectAttributeRequest
*/
objectType?: string;
/**
*
* @type {boolean}
* @memberof PatchedObjectAttributeRequest
*/
enabled?: boolean;
/**
*
* @type {string}
* @memberof PatchedObjectAttributeRequest
*/
key?: string;
/**
*
* @type {string}
* @memberof PatchedObjectAttributeRequest
*/
label?: string;
/**
*
* @type {string}
* @memberof PatchedObjectAttributeRequest
*/
regex?: string;
/**
*
* @type {ObjectAttributeTypeEnum}
* @memberof PatchedObjectAttributeRequest
*/
type?: ObjectAttributeTypeEnum;
/**
*
* @type {string}
* @memberof PatchedObjectAttributeRequest
*/
group?: string;
/**
* Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.
* @type {string}
* @memberof PatchedObjectAttributeRequest
*/
managed?: string | null;
/**
*
* @type {boolean}
* @memberof PatchedObjectAttributeRequest
*/
isUnique?: boolean;
/**
*
* @type {boolean}
* @memberof PatchedObjectAttributeRequest
*/
isRequired?: boolean;
/**
*
* @type {boolean}
* @memberof PatchedObjectAttributeRequest
*/
isArray?: boolean;
}
/**
* Check if a given object implements the PatchedObjectAttributeRequest interface.
*/
export function instanceOfPatchedObjectAttributeRequest(
value: object,
): value is PatchedObjectAttributeRequest {
return true;
}
export function PatchedObjectAttributeRequestFromJSON(json: any): PatchedObjectAttributeRequest {
return PatchedObjectAttributeRequestFromJSONTyped(json, false);
}
export function PatchedObjectAttributeRequestFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): PatchedObjectAttributeRequest {
if (json == null) {
return json;
}
return {
objectType: json["object_type"] == null ? undefined : json["object_type"],
enabled: json["enabled"] == null ? undefined : json["enabled"],
key: json["key"] == null ? undefined : json["key"],
label: json["label"] == null ? undefined : json["label"],
regex: json["regex"] == null ? undefined : json["regex"],
type: json["type"] == null ? undefined : ObjectAttributeTypeEnumFromJSON(json["type"]),
group: json["group"] == null ? undefined : json["group"],
managed: json["managed"] == null ? undefined : json["managed"],
isUnique: json["is_unique"] == null ? undefined : json["is_unique"],
isRequired: json["is_required"] == null ? undefined : json["is_required"],
isArray: json["is_array"] == null ? undefined : json["is_array"],
};
}
export function PatchedObjectAttributeRequestToJSON(json: any): PatchedObjectAttributeRequest {
return PatchedObjectAttributeRequestToJSONTyped(json, false);
}
export function PatchedObjectAttributeRequestToJSONTyped(
value?: PatchedObjectAttributeRequest | null,
ignoreDiscriminator: boolean = false,
): any {
if (value == null) {
return value;
}
return {
object_type: value["objectType"],
enabled: value["enabled"],
key: value["key"],
label: value["label"],
regex: value["regex"],
type: ObjectAttributeTypeEnumToJSON(value["type"]),
group: value["group"],
managed: value["managed"],
is_unique: value["isUnique"],
is_required: value["isRequired"],
is_array: value["isArray"],
};
}

View File

@@ -344,6 +344,9 @@ export * from "./OAuthSource";
export * from "./OAuthSourcePropertyMapping";
export * from "./OAuthSourcePropertyMappingRequest";
export * from "./OAuthSourceRequest";
export * from "./ObjectAttribute";
export * from "./ObjectAttributeRequest";
export * from "./ObjectAttributeTypeEnum";
export * from "./OpenIDConnectConfiguration";
export * from "./OperatingSystem";
export * from "./OperatingSystemRequest";
@@ -439,6 +442,7 @@ export * from "./PaginatedNotificationWebhookMappingList";
export * from "./PaginatedOAuth2ProviderList";
export * from "./PaginatedOAuthSourceList";
export * from "./PaginatedOAuthSourcePropertyMappingList";
export * from "./PaginatedObjectAttributeList";
export * from "./PaginatedOutpostList";
export * from "./PaginatedPasswordExpiryPolicyList";
export * from "./PaginatedPasswordPolicyList";
@@ -595,6 +599,7 @@ export * from "./PatchedNotificationWebhookMappingRequest";
export * from "./PatchedOAuth2ProviderRequest";
export * from "./PatchedOAuthSourcePropertyMappingRequest";
export * from "./PatchedOAuthSourceRequest";
export * from "./PatchedObjectAttributeRequest";
export * from "./PatchedOutpostRequest";
export * from "./PatchedPasswordExpiryPolicyRequest";
export * from "./PatchedPasswordPolicyRequest";

View File

@@ -134,6 +134,12 @@ paths:
get:
operationId: admin_models_list
description: Read-only view list all installed models
parameters:
- in: query
name: filter_has_attributes
schema:
type: boolean
nullable: true
tags:
- admin
security:
@@ -3720,6 +3726,172 @@ paths:
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/core/object_attributes/:
get:
operationId: core_object_attributes_list
parameters:
- in: query
name: enabled
schema:
type: boolean
- in: query
name: object_type__app_label
schema:
type: string
- in: query
name: object_type__model
schema:
type: string
- $ref: '#/components/parameters/QueryPaginationOrdering'
- $ref: '#/components/parameters/QueryPaginationPage'
- $ref: '#/components/parameters/QueryPaginationPageSize'
- $ref: '#/components/parameters/QuerySearch'
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedObjectAttributeList'
description: ''
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
post:
operationId: core_object_attributes_create
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectAttributeRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectAttribute'
description: ''
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/core/object_attributes/{attribute_id}/:
get:
operationId: core_object_attributes_retrieve
parameters:
- in: path
name: attribute_id
schema:
type: string
format: uuid
description: A UUID string identifying this Object Attribute.
required: true
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectAttribute'
description: ''
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
put:
operationId: core_object_attributes_update
parameters:
- in: path
name: attribute_id
schema:
type: string
format: uuid
description: A UUID string identifying this Object Attribute.
required: true
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectAttributeRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectAttribute'
description: ''
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
patch:
operationId: core_object_attributes_partial_update
parameters:
- in: path
name: attribute_id
schema:
type: string
format: uuid
description: A UUID string identifying this Object Attribute.
required: true
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedObjectAttributeRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectAttribute'
description: ''
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
delete:
operationId: core_object_attributes_destroy
parameters:
- in: path
name: attribute_id
schema:
type: string
format: uuid
description: A UUID string identifying this Object Attribute.
required: true
tags:
- core
security:
- authentik: []
responses:
'204':
description: No response body
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/core/tokens/:
get:
operationId: core_tokens_list
@@ -36858,8 +37030,12 @@ components:
verbose_name_plural:
type: string
readOnly: true
fully_qualified_model:
type: string
readOnly: true
required:
- app_label
- fully_qualified_model
- id
- model
- verbose_name_plural
@@ -43421,6 +43597,7 @@ components:
- authentik_core.application
- authentik_core.applicationentitlement
- authentik_core.token
- authentik_core.objectattribute
- authentik_crypto.certificatekeypair
- authentik_endpoints.deviceuserbinding
- authentik_endpoints.deviceaccessgroup
@@ -44702,6 +44879,111 @@ components:
- name
- provider_type
- slug
ObjectAttribute:
type: object
properties:
pk:
type: string
format: uuid
readOnly: true
title: Attribute id
object_type:
type: string
object_type_obj:
allOf:
- $ref: '#/components/schemas/ContentType'
readOnly: true
enabled:
type: boolean
default: true
created:
type: string
format: date-time
readOnly: true
key:
type: string
label:
type: string
last_updated:
type: string
format: date-time
readOnly: true
regex:
type: string
type:
$ref: '#/components/schemas/ObjectAttributeTypeEnum'
group:
type: string
managed:
type: string
nullable: true
title: Managed by authentik
description: Objects that are managed by authentik. These objects are created
and updated automatically. This flag only indicates that an object can
be overwritten by migrations. You can still modify the objects via the
API, but expect changes to be overwritten in a later update.
is_unique:
type: boolean
is_required:
type: boolean
is_array:
type: boolean
required:
- created
- key
- label
- last_updated
- object_type
- object_type_obj
- pk
- type
ObjectAttributeRequest:
type: object
properties:
object_type:
type: string
minLength: 1
enabled:
type: boolean
default: true
key:
type: string
minLength: 1
label:
type: string
minLength: 1
regex:
type: string
type:
$ref: '#/components/schemas/ObjectAttributeTypeEnum'
group:
type: string
managed:
type: string
nullable: true
minLength: 1
title: Managed by authentik
description: Objects that are managed by authentik. These objects are created
and updated automatically. This flag only indicates that an object can
be overwritten by migrations. You can still modify the objects via the
API, but expect changes to be overwritten in a later update.
is_unique:
type: boolean
is_required:
type: boolean
is_array:
type: boolean
required:
- key
- label
- object_type
- type
ObjectAttributeTypeEnum:
enum:
- text
- number
- boolean
type: string
OpenIDConnectConfiguration:
type: object
description: rest_framework Serializer for OIDC Configuration
@@ -46252,6 +46534,21 @@ components:
- autocomplete
- pagination
- results
PaginatedObjectAttributeList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/ObjectAttribute'
autocomplete:
$ref: '#/components/schemas/Autocomplete'
required:
- autocomplete
- pagination
- results
PaginatedOutpostList:
type: object
properties:
@@ -50034,6 +50331,42 @@ components:
- $ref: '#/components/schemas/AuthorizationCodeAuthMethodEnum'
description: How to perform authentication during an authorization_code
token request flow
PatchedObjectAttributeRequest:
type: object
properties:
object_type:
type: string
minLength: 1
enabled:
type: boolean
default: true
key:
type: string
minLength: 1
label:
type: string
minLength: 1
regex:
type: string
type:
$ref: '#/components/schemas/ObjectAttributeTypeEnum'
group:
type: string
managed:
type: string
nullable: true
minLength: 1
title: Managed by authentik
description: Objects that are managed by authentik. These objects are created
and updated automatically. This flag only indicates that an object can
be overwritten by migrations. You can still modify the objects via the
API, but expect changes to be overwritten in a later update.
is_unique:
type: boolean
is_required:
type: boolean
is_array:
type: boolean
PatchedOutpostRequest:
type: object
description: Outpost Serializer

View File

@@ -87,6 +87,10 @@ export const ROUTES: Route[] = [
await import("#admin/policies/reputation/ReputationListPage");
return html`<ak-policy-reputation-list></ak-policy-reputation-list>`;
}),
new Route(new RegExp("^/identity/object-attributes$"), async () => {
await import("#admin/object-attributes/ObjectAttributeListPage");
return html`<ak-object-attribute-list></ak-object-attribute-list>`;
}),
new Route(new RegExp("^/identity/groups$"), async () => {
await import("#admin/groups/GroupListPage");
return html`<ak-group-list></ak-group-list>`;

View File

@@ -1,15 +1,12 @@
import "#elements/CodeMirror";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/Radio";
import "#elements/forms/SearchSelect/index";
import { DEFAULT_CONFIG } from "#common/api/config";
import { ModelForm } from "#elements/forms/ModelForm";
import { ObjectAttributeModelForm } from "#admin/object-attributes/renderAttributes";
import { ApplicationEntitlement, CoreApi } from "@goauthentik/api";
import YAML from "yaml";
import { ApplicationEntitlement, CoreApi, ModelEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
@@ -18,7 +15,12 @@ import { customElement, property } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
@customElement("ak-application-entitlement-form")
export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement, string> {
export class ApplicationEntitlementForm extends ObjectAttributeModelForm<
ApplicationEntitlement,
string
> {
public model = ModelEnum.AuthentikCoreApplicationentitlement;
async loadInstance(pk: string): Promise<ApplicationEntitlement> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsRetrieve({
pbmUuid: pk,
@@ -61,16 +63,7 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
<ak-codemirror
mode="yaml"
value="${YAML.stringify(this.instance?.attributes ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
</ak-form-element-horizontal>`;
${this.renderObjectAttributes(this.objAttributes, this.instance)}`;
}
}

View File

@@ -4,10 +4,16 @@ import "#elements/forms/HorizontalFormElement";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PFSize } from "#common/enums";
import { ModelForm } from "#elements/forms/ModelForm";
import { WithBrandConfig } from "#elements/mixins/branding";
import { DeviceAccessGroup, DeviceAccessGroupRequest, EndpointsApi } from "@goauthentik/api";
import { ObjectAttributeModelForm } from "#admin/object-attributes/renderAttributes";
import {
DeviceAccessGroup,
DeviceAccessGroupRequest,
EndpointsApi,
ModelEnum,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html } from "lit";
@@ -20,7 +26,11 @@ import { ifDefined } from "lit/directives/if-defined.js";
* @prop {string} instancePk - The primary key of the instance to load.
*/
@customElement("ak-endpoints-device-access-groups-form")
export class DeviceAccessGroupForm extends WithBrandConfig(ModelForm<DeviceAccessGroup, string>) {
export class DeviceAccessGroupForm extends WithBrandConfig(
ObjectAttributeModelForm<DeviceAccessGroup, string>,
) {
public model = ModelEnum.AuthentikEndpointsDeviceaccessgroup;
public static override verboseName = msg("Device Access Group");
public static override verboseNamePlural = msg("Device Access Groups");
@@ -53,13 +63,14 @@ export class DeviceAccessGroupForm extends WithBrandConfig(ModelForm<DeviceAcces
protected override renderForm() {
return html`<ak-text-input
name="name"
autocomplete="off"
placeholder=${msg("Type a group name...")}
label=${msg("Group Name")}
value=${ifDefined(this.instance?.name)}
required
></ak-text-input>`;
name="name"
autocomplete="off"
placeholder=${msg("Type a group name...")}
label=${msg("Group Name")}
value=${ifDefined(this.instance?.name)}
required
></ak-text-input>
${this.renderObjectAttributes(this.objAttributes, this.instance)}`;
}
}

View File

@@ -1,21 +1,20 @@
import "#admin/groups/ak-group-member-table";
import "#components/ak-switch-input";
import "#components/ak-text-input";
import "#elements/CodeMirror";
import "#elements/ak-dual-select/ak-dual-select-provider";
import "#elements/chips/Chip";
import "#elements/chips/ChipGroup";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/SearchSelect/index";
import "#components/ak-text-input";
import "#components/ak-switch-input";
import { DEFAULT_CONFIG } from "#common/api/config";
import { DataProvision, DualSelectPair } from "#elements/ak-dual-select/types";
import { ModelForm } from "#elements/forms/ModelForm";
import { CoreApi, Group, RbacApi, RelatedGroup, Role } from "@goauthentik/api";
import { ObjectAttributeModelForm } from "#admin/object-attributes/renderAttributes";
import YAML from "yaml";
import { CoreApi, Group, ModelEnum, RbacApi, RelatedGroup, Role } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
@@ -30,7 +29,8 @@ export function rbacRolePair(item: Role): DualSelectPair {
}
@customElement("ak-group-form")
export class GroupForm extends ModelForm<Group, string> {
export class GroupForm extends ObjectAttributeModelForm<Group, string> {
public model = ModelEnum.AuthentikCoreGroup;
static styles: CSSResult[] = [
...super.styles,
css`
@@ -144,16 +144,7 @@ export class GroupForm extends ModelForm<Group, string> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
<ak-codemirror
mode="yaml"
value="${YAML.stringify(this.instance?.attributes ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
</ak-form-element-horizontal>`;
${this.renderObjectAttributes(this.objAttributes, this.instance)}`;
}
}

View File

@@ -93,6 +93,7 @@ export const createAdminSidebarEntries = (): readonly SidebarEntry[] => [
["/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/object-attributes", msg("Object attributes")],
["/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")],

View File

@@ -0,0 +1,163 @@
import "#elements/forms/FormGroup";
import "#elements/forms/HorizontalFormElement";
import "#elements/forms/Radio";
import "#elements/forms/SearchSelect/index";
import "#components/ak-text-input";
import "#components/ak-switch-input";
import { DEFAULT_CONFIG } from "#common/api/config";
import { ModelForm } from "#elements/forms/ModelForm";
import {
AdminApi,
AdminModelsListRequest,
App,
CoreApi,
ObjectAttribute,
ObjectAttributeTypeEnum,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("ak-object-attribute-form")
export class ObjectAttributeForm extends ModelForm<ObjectAttribute, string> {
async loadInstance(pk: string): Promise<ObjectAttribute> {
return await new CoreApi(DEFAULT_CONFIG).coreObjectAttributesRetrieve({
attributeId: pk,
});
}
getSuccessMessage(): string {
return this.instance
? msg("Successfully updated attribute.")
: msg("Successfully created attribute.");
}
async send(data: ObjectAttribute): Promise<ObjectAttribute> {
data.regex = data.regex !== "" ? data.regex : undefined;
if (this.instance?.pk) {
return new CoreApi(DEFAULT_CONFIG).coreObjectAttributesUpdate({
attributeId: this.instance.pk,
objectAttributeRequest: data,
});
}
return new CoreApi(DEFAULT_CONFIG).coreObjectAttributesCreate({
objectAttributeRequest: data,
});
}
//#region Renders
protected override renderForm(): TemplateResult {
return html`<ak-text-input
name="label"
value="${this.instance?.label ?? ""}"
label=${msg("Label")}
placeholder=${msg("Type a human-readable name...")}
required
></ak-text-input>
<ak-text-input
name="key"
value="${this.instance?.key ?? ""}"
label=${msg("Key")}
placeholder=${msg("Type a unique identifier...")}
required
></ak-text-input>
<ak-text-input
name="group"
value="${this.instance?.group ?? ""}"
label=${msg("Group")}
placeholder=${msg("Type an optional group identifier...")}
></ak-text-input>
<ak-switch-input
name="enabled"
label=${msg("Enabled")}
?checked=${this.instance?.enabled ?? true}
help=${msg("Value of the attribute cannot be empty.")}
></ak-switch-input>
<ak-form-element-horizontal label=${msg("Type")} required name="type">
<ak-radio
.options=${[
{
label: msg("Text"),
value: ObjectAttributeTypeEnum.Text,
default: true,
},
{
label: msg("Number"),
value: ObjectAttributeTypeEnum.Number,
},
{
label: msg("Boolean"),
value: ObjectAttributeTypeEnum.Boolean,
},
]}
.value=${this.instance?.type}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal label="Object type" name="objectType" required>
<ak-search-select
.fetchObjects=${async (): Promise<App[]> => {
const args: AdminModelsListRequest = {
filterHasAttributes: true,
};
return await new AdminApi(DEFAULT_CONFIG).adminModelsList(args);
}}
.renderElement=${(app: App): string => {
return app.label;
}}
.value=${(app: App | undefined): string | undefined => {
return app?.name;
}}
.selected=${(app: App): boolean => {
return app.name === this.instance?.objectTypeObj.fullyQualifiedModel;
}}
>
</ak-search-select>
</ak-form-element-horizontal>
<ak-form-group label=${msg("Validation")} open>
<div class="pf-c-form">
<ak-switch-input
name="isRequired"
label=${msg("Attribute is required")}
?checked=${this.instance?.isRequired}
help=${msg("Value of the attribute cannot be empty.")}
></ak-switch-input>
<ak-switch-input
name="isUnique"
label=${msg("Attribute is unique")}
?checked=${this.instance?.isUnique}
help=${msg(
"Value of the attribute must be unique across all instances of the selected object type.",
)}
></ak-switch-input>
<ak-switch-input
name="isArray"
label=${msg("Attribute is an array")}
?checked=${this.instance?.isArray}
help=${msg("Value can have multiple entries.")}
></ak-switch-input>
<ak-text-input
name="regex"
value="${this.instance?.regex ?? ""}"
label=${msg("RegEx")}
input-hint="code"
placeholder=${msg("Enter a regex for validation...")}
></ak-text-input>
</div>
</ak-form-group>`;
}
//#endregion
}
declare global {
interface HTMLElementTagNameMap {
"ak-object-attribute-form": ObjectAttributeForm;
}
}

View File

@@ -0,0 +1,129 @@
import "#admin/rbac/ObjectPermissionModal";
import "#admin/object-attributes/ObjectAttributeForm";
import "#elements/forms/DeleteBulkForm";
import "#elements/forms/ModalForm";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import "#components/ak-status-label";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { CoreApi, ObjectAttribute, ObjectAttributeTypeEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
export function objectAttributeTypeToLabel(type?: ObjectAttributeTypeEnum): string {
if (!type) return "";
switch (type) {
case ObjectAttributeTypeEnum.Text:
return msg("Text");
case ObjectAttributeTypeEnum.Number:
return msg("Number");
case ObjectAttributeTypeEnum.Boolean:
return msg("Boolean");
}
return msg("Unknown type");
}
@customElement("ak-object-attribute-list")
export class ObjectAttributeListPage extends TablePage<ObjectAttribute> {
protected override searchEnabled = true;
public pageTitle = msg("Object attributes");
public pageDescription = "Configure attributes on objects such as users and groups.";
public pageIcon = "pf-icon pf-icon-flavor";
protected override rowLabel(item: ObjectAttribute): string | null {
return item.pk ?? null;
}
checkbox = true;
clearOnRefresh = true;
@property()
order = "key";
async apiEndpoint(): Promise<PaginatedResponse<ObjectAttribute>> {
return new CoreApi(DEFAULT_CONFIG).coreObjectAttributesList(
await this.defaultEndpointConfig(),
);
}
protected columns: TableColumn[] = [
[msg("Label"), "label"],
[msg("Type"), "type"],
[msg("Status"), "enabled"],
[msg("Object type"), "object_type"],
[msg("Actions"), null, msg("Row Actions")],
];
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
object-label=${msg("Object Attribute(s)")}
.objects=${this.selectedElements}
.metadata=${(item: ObjectAttribute) => {
return [
{ key: msg("Object type"), value: item.objectTypeObj.verboseNamePlural },
{ key: msg("Label"), value: item.label },
{ key: msg("Key"), value: item.key },
];
}}
.delete=${(item: ObjectAttribute) => {
return new CoreApi(DEFAULT_CONFIG).coreObjectAttributesDestroy({
attributeId: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit">${msg("Create")}</span>
<span slot="header">${msg("New Attribute")}</span>
<ak-object-attribute-form slot="form"> </ak-object-attribute-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
}
row(item: ObjectAttribute): SlottedTemplateResult[] {
return [
html`<div>
<div>${item.group}: ${item.label}</div>
<code>${item.key}</code>
</div>`,
html`${objectAttributeTypeToLabel(item.type)}`,
html`<ak-status-label ?good=${item.enabled} type="info"></ak-status-label>`,
html`${item.objectTypeObj.verboseNamePlural}`,
html`<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header">${msg("Update Attribute")}</span>
<ak-object-attribute-form
slot="form"
.instancePk=${item.pk}
></ak-object-attribute-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-forms-modal> `,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-object-attribute-list": ObjectAttributeListPage;
}
}

View File

@@ -0,0 +1,104 @@
import "#components/ak-text-input";
import "#components/ak-switch-input";
import "#components/ak-number-input";
import "#elements/forms/FormGroup";
import "#elements/CodeMirror/ak-codemirror";
import { DEFAULT_CONFIG } from "#common/api/config";
import { groupBy } from "#common/utils";
import { ModelForm } from "#elements/forms/ModelForm";
import { CoreApi, ModelEnum, ObjectAttribute, ObjectAttributeTypeEnum } from "@goauthentik/api";
import YAML from "yaml";
import { msg } from "@lit/localize";
import { html, nothing } from "lit-html";
import { state } from "lit/decorators.js";
export interface ObjectAttributeOptions {
disableRawAttributes: boolean;
}
export type AttributesMixin = {
attributes?: { [key: string]: unknown };
};
export abstract class ObjectAttributeModelForm<
T extends object = object,
PKT extends string | number = string | number,
D = T,
> extends ModelForm<T, PKT, D> {
@state()
objAttributes: ObjectAttribute[] = [];
public abstract model: ModelEnum;
async load() {
const [app, model] = this.model.split(".");
this.objAttributes = (
await new CoreApi(DEFAULT_CONFIG).coreObjectAttributesList({
objectTypeAppLabel: app,
objectTypeModel: model,
enabled: true,
})
).results;
}
renderObjectAttributes(
defs: ObjectAttribute[],
obj: AttributesMixin | null,
options?: ObjectAttributeOptions,
) {
const attrs = obj?.attributes || {};
const renderSingleAttribute = (attr: ObjectAttribute) => {
switch (attr.type) {
case ObjectAttributeTypeEnum.Text:
return html`<ak-text-input
name="attributes.${attr.key}"
label=${attr.label}
autocomplete="off"
.value="${attrs[attr.key]}"
?required=${attr.isRequired}
></ak-text-input>`;
case ObjectAttributeTypeEnum.Number:
return html`<ak-number-input
name="attributes.${attr.key}"
label=${attr.label}
.value="${attrs[attr.key]}"
?required=${attr.isRequired}
></ak-number-input>`;
case ObjectAttributeTypeEnum.Boolean:
return html`<ak-switch-input
name="attributes.${attr.key}"
label=${attr.label}
?checked=${attrs[attr.key]}
?required=${attr.isRequired}
>
</ak-switch-input>`;
}
};
return html`${groupBy(defs, (def) => def.group || "").map(([group, attrs]) => {
if (group === "") {
return html`${attrs.map((attr) => renderSingleAttribute(attr))}`;
}
return html`<ak-form-group label=${group}>
<div class="pf-c-form">${attrs.map((attr) => renderSingleAttribute(attr))}</div>
</ak-form-group>`;
})}
${options?.disableRawAttributes
? nothing
: html`<ak-form-group label=${msg("Advanced settings")}>
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
<ak-codemirror mode="yaml" value="${YAML.stringify(attrs)}">
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`}`;
}
}

View File

@@ -8,14 +8,14 @@ import "#components/ak-switch-input";
import { DEFAULT_CONFIG } from "#common/api/config";
import { ModelForm } from "#elements/forms/ModelForm";
import { RadioOption } from "#elements/forms/Radio";
import { SlottedTemplateResult } from "#elements/types";
import { CoreApi, Group, RbacApi, Role, User, UserTypeEnum } from "@goauthentik/api";
import { ObjectAttributeModelForm } from "#admin/object-attributes/renderAttributes";
import { CoreApi, Group, ModelEnum, RbacApi, Role, User, UserTypeEnum } from "@goauthentik/api";
import { match } from "ts-pattern";
import YAML from "yaml";
import { msg, str } from "@lit/localize";
import { css, CSSResult, html } from "lit";
@@ -44,8 +44,11 @@ const UserTypeOptions: readonly RadioOption<UserTypeEnum>[] = [
description: html`${msg("Machine-to-machine authentication or other automations.")}`,
},
];
@customElement("ak-user-form")
export class UserForm extends ModelForm<User, number> {
export class UserForm extends ObjectAttributeModelForm<User, number> {
public model = ModelEnum.AuthentikCoreUser;
#coreAPI = new CoreApi(DEFAULT_CONFIG);
#rbacAPI = new RbacApi(DEFAULT_CONFIG);
@@ -242,7 +245,6 @@ export class UserForm extends ModelForm<User, number> {
)}
>
</ak-switch-input>
<ak-text-input
name="path"
label=${msg("Path")}
@@ -263,18 +265,7 @@ export class UserForm extends ModelForm<User, number> {
</p>`}
></ak-text-input>
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
<ak-codemirror
mode="yaml"
value="${YAML.stringify(
this.instance?.attributes ?? UserForm.defaultUserAttributes,
)}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
</ak-form-element-horizontal>`;
${this.renderObjectAttributes(this.objAttributes, this.instance)}`;
}
}

View File

@@ -5,6 +5,8 @@ import { isControlElement } from "#elements/ControlElement";
import { isFormField } from "#elements/forms/form-associated-element";
import { isNamedElement, NamedElement } from "#elements/utils/inputs";
import { deepmerge } from "deepmerge-ts";
function isIgnored<T extends Element>(element: T) {
if (!(element instanceof HTMLElement)) return false;
@@ -22,7 +24,7 @@ function assignValue(
let parent = destination;
if (!element.name?.includes(".")) {
parent[element.name] = value;
parent[element.name] = deepmerge(parent[element.name], value);
return;
}