Compare commits

..

35 Commits

Author SHA1 Message Date
Jens Langhammer
b96c9cf1d6 Merge branch 'main' into core/object-attributes 2026-05-13 13:54:34 +02:00
dependabot[bot]
a3c50ae92a core: bump django-stubs[compatible-mypy] from 6.0.3 to 6.0.4 (#22319)
Bumps [django-stubs[compatible-mypy]](https://github.com/typeddjango/django-stubs) from 6.0.3 to 6.0.4.
- [Release notes](https://github.com/typeddjango/django-stubs/releases)
- [Commits](https://github.com/typeddjango/django-stubs/compare/6.0.3...6.0.4)

---
updated-dependencies:
- dependency-name: django-stubs[compatible-mypy]
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 13:49:57 +02:00
dependabot[bot]
3ef36b9e9e ci: bump taiki-e/install-action from 2.77.3 to 2.77.4 in /.github/actions/setup (#22321)
ci: bump taiki-e/install-action in /.github/actions/setup

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.77.3 to 2.77.4.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](e3134ec54b...ec28e28791)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.77.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 13:49:53 +02:00
Simonyi Gergő
691e173cad endpoints: remove print line (#22325) 2026-05-13 13:45:28 +02:00
Dewi Roberts
68a6b04749 website/docs: release notes 2026.5: add section about package reduction (#22308)
* Add section about package reduction

* Suggestion from marc

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
2026-05-13 08:27:24 +01:00
authentik-automation[bot]
046dbdabe2 core, web: update translations (#22318)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-05-13 09:27:14 +02:00
authentik-automation[bot]
aae1b32c61 stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#22322)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-05-13 09:25:56 +02:00
Marcelo Elizeche Landó
87a95eddea website/docs: Add invitation wizard docs (#22069)
* Add invitation wizard docs

* Apply suggestions from code review

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* Apply suggestion from @dominic-r

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* Add title to info box

* Apply suggestion from @dominic-r

Signed-off-by: Dominic R <dominic@goauthentik.io>

* Apply suggestions from code review

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Signed-off-by: Dominic R <dominic@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-05-12 18:35:28 -05: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
56 changed files with 2777 additions and 132 deletions

View File

@@ -64,7 +64,7 @@ runs:
rustflags: ""
- name: Setup rust dependencies
if: ${{ contains(inputs.dependencies, 'rust') }}
uses: taiki-e/install-action@e3134ec54b36203e18f2d1e80652058bd078dd91 # v2
uses: taiki-e/install-action@ec28e287910af896fd98e04056d31fa68607e7ad # v2
with:
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
- name: Setup node (web)

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

@@ -31,7 +31,6 @@ class DeviceUser(VirtualUser):
username = "authentik:endpoints:device"
def has_perm(self, perm: str, obj: Model | None = None) -> bool:
print(perm)
if perm in [
"authentik_core.view_user",
"authentik_core.view_group",

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

File diff suppressed because one or more lines are too long

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

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-06 00:27+0000\n"
"POT-Creation-Date: 2026-05-13 05:39+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -226,6 +226,10 @@ msgstr ""
msgid "The slug '{slug}' is reserved and cannot be used for applications."
msgstr ""
#: authentik/core/api/groups.py
msgid "User does not have permission to add members to this group."
msgstr ""
#: authentik/core/api/providers.py
msgid ""
"When not set all providers are returned. When set to true, only backchannel "
@@ -256,6 +260,14 @@ msgstr ""
msgid "Setting a user to internal service account is not allowed."
msgstr ""
#: authentik/core/api/users.py
msgid "User does not have permission to add members to a superuser group."
msgstr ""
#: authentik/core/api/users.py
msgid "User does not have permission to assign roles."
msgstr ""
#: authentik/core/api/users.py
msgid "Can't modify internal service account users"
msgstr ""

Binary file not shown.

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

@@ -85,7 +85,7 @@ dev = [
"coverage[toml]==7.13.5",
"daphne==4.2.1",
"debugpy==1.8.20",
"django-stubs[compatible-mypy]==6.0.3",
"django-stubs[compatible-mypy]==6.0.4",
"djangorestframework-stubs[compatible-mypy]==3.16.9",
"drf-jsonschema-serializer==3.0.0",
"freezegun==1.5.5",

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

8
uv.lock generated
View File

@@ -394,7 +394,7 @@ dev = [
{ name = "coverage", extras = ["toml"], specifier = "==7.13.5" },
{ name = "daphne", specifier = "==4.2.1" },
{ name = "debugpy", specifier = "==1.8.20" },
{ name = "django-stubs", extras = ["compatible-mypy"], specifier = "==6.0.3" },
{ name = "django-stubs", extras = ["compatible-mypy"], specifier = "==6.0.4" },
{ name = "djangorestframework-stubs", extras = ["compatible-mypy"], specifier = "==3.16.9" },
{ name = "drf-jsonschema-serializer", specifier = "==3.0.0" },
{ name = "freezegun", specifier = "==1.5.5" },
@@ -1269,7 +1269,7 @@ s3 = [
[[package]]
name = "django-stubs"
version = "6.0.3"
version = "6.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
@@ -1277,9 +1277,9 @@ dependencies = [
{ name = "types-pyyaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/0c/8d0d875af79bf774c1c3997c84aa118dba3a77be12086b9c14e130e8ec72/django_stubs-6.0.3.tar.gz", hash = "sha256:ee895f403c373608eeb50822f0733f9d9ec5ab12731d4ab58956053bb95fdd9e", size = 278214, upload-time = "2026-04-18T15:11:22.327Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/82/ccf2a2dc9cdb4bd9cbe91f11e887589bf2da7609506db00ccbc73bd8a6da/django_stubs-6.0.4.tar.gz", hash = "sha256:7aee77e8de9c14c0d9cf84988befe826d93cbc15a87e0ade2943f14d553451cf", size = 280019, upload-time = "2026-05-09T21:24:30.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/a3/6751b7684d20fc4f228bdd3dd8341d382ab3faaf65d3d050c0d59ab0a1b0/django_stubs-6.0.3-py3-none-any.whl", hash = "sha256:5fee22bcbbad59a78c727a820b6f4e68ff442ca76a922b7002e57c25dd7cb390", size = 541570, upload-time = "2026-04-18T15:11:20.711Z" },
{ url = "https://files.pythonhosted.org/packages/ba/e7/5128914ada94dd6277626ef5a4a5680a4def7d2f9366214d26c1cd86723b/django_stubs-6.0.4-py3-none-any.whl", hash = "sha256:e991c68f77239663577a5f4fc75e99c84f867f378cafc97cbf4acc5aff378279", size = 543791, upload-time = "2026-05-09T21:24:28.218Z" },
]
[package.optional-dependencies]

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

@@ -29,16 +29,15 @@ import { renderDialog } from "#elements/dialogs";
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { WithNotifications } from "#elements/mixins/notifications";
import { canAccessAdmin, WithSession } from "#elements/mixins/session";
import { navigate } from "#elements/router/RouterOutlet";
import { SlottedTemplateResult } from "#elements/types";
import { AKDrawerChangeEvent } from "#components/notifications/events";
import { AKDrawerChangeEvent } from "#elements/notifications/events";
import {
DrawerState,
persistDrawerParams,
readDrawerParams,
renderNotificationDrawerPanel,
} from "#components/notifications/utils";
} from "#elements/notifications/utils";
import { navigate } from "#elements/router/RouterOutlet";
import { SlottedTemplateResult } from "#elements/types";
import Styles from "#admin/ak-interface-admin.css";
import { ROUTES } from "#admin/Routes";

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

@@ -10,10 +10,10 @@ import { formatUserDisplayName } from "#common/users";
import { AKElement } from "#elements/Base";
import { WithNotifications } from "#elements/mixins/notifications";
import { WithSession } from "#elements/mixins/session";
import { AKDrawerChangeEvent } from "#elements/notifications/events";
import { isDefaultAvatar } from "#elements/utils/images";
import Styles from "#components/ak-nav-button.css";
import { AKDrawerChangeEvent } from "#components/notifications/events";
import { CoreApi } from "@goauthentik/api";

View File

@@ -13,10 +13,9 @@ import {
NotificationsMixin,
} from "#elements/mixins/notifications";
import { SessionMixin } from "#elements/mixins/session";
import { createPaginatedNotificationListFrom } from "#elements/notifications/utils";
import type { ReactiveElementHost } from "#elements/types";
import { createPaginatedNotificationListFrom } from "#components/notifications/utils";
import { EventsApi } from "@goauthentik/api";
import { ContextProvider } from "@lit/context";

View File

@@ -20,10 +20,9 @@ import {
SessionMixin,
UIConfigContext,
} from "#elements/mixins/session";
import { AKDrawerChangeEvent } from "#elements/notifications/events";
import type { ReactiveElementHost } from "#elements/types";
import { AKDrawerChangeEvent } from "#components/notifications/events";
import { CoreApi, SessionUser } from "@goauthentik/api";
import { setUser } from "@sentry/browser";

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;
}

View File

@@ -4,11 +4,10 @@ import { MessageLevel } from "#common/messages";
import { ContextControllerRegistry } from "#elements/controllers/ContextControllerRegistry";
import { showMessage } from "#elements/messages/MessageContainer";
import { AKDrawerChangeEvent } from "#elements/notifications/events";
import { createPaginatedNotificationListFrom } from "#elements/notifications/utils";
import { createMixin } from "#elements/types";
import { AKDrawerChangeEvent } from "#components/notifications/events";
import { createPaginatedNotificationListFrom } from "#components/notifications/utils";
import { ConsoleLogger } from "#logger/browser";
import {

View File

@@ -5,8 +5,7 @@ import { globalAK } from "#common/global";
import { AKElement } from "#elements/Base";
import { listen } from "#elements/decorators/listen";
import { AKDrawerChangeEvent } from "#components/notifications/events";
import { AKDrawerChangeEvent } from "#elements/notifications/events";
import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
@@ -81,9 +80,14 @@ export class APIDrawer extends AKElement {
@listen(AKRequestPostEvent, { target: window })
protected enqueueRequest = ({ requestInfo }: AKRequestPostEvent) => {
this.requests = [requestInfo, ...this.requests]
.toSorted((a, b) => b.time - a.time)
.slice(0, 50);
this.requests.push(requestInfo);
this.requests.sort((a, b) => a.time - b.time).reverse();
if (this.requests.length > 50) {
this.requests.shift();
}
this.requestUpdate();
};
render(): TemplateResult {

View File

@@ -10,11 +10,10 @@ import { formatElapsedTime } from "#common/temporal";
import { AKElement } from "#elements/Base";
import { WithNotifications } from "#elements/mixins/notifications";
import { WithSession } from "#elements/mixins/session";
import { AKDrawerChangeEvent } from "#elements/notifications/events";
import { SlottedTemplateResult } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { AKDrawerChangeEvent } from "#components/notifications/events";
import { Notification } from "@goauthentik/api";
import { msg, str } from "@lit/localize";

View File

@@ -1,4 +1,4 @@
import { DrawerState, readDrawerParams } from "#components/notifications/utils";
import { DrawerState, readDrawerParams } from "#elements/notifications/utils";
/**
* Event dispatched when the state of the interface drawers changes.

View File

@@ -2,8 +2,8 @@
* @file Notification drawer utilities.
*/
import "#components/notifications/APIDrawer";
import "#components/notifications/NotificationDrawer";
import "#elements/notifications/APIDrawer";
import "#elements/notifications/NotificationDrawer";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";

View File

@@ -1,7 +1,7 @@
import "#components/ak-nav-buttons";
import "#elements/banner/EnterpriseStatusBanner";
import "#components/notifications/APIDrawer";
import "#components/notifications/NotificationDrawer";
import "#elements/notifications/APIDrawer";
import "#elements/notifications/NotificationDrawer";
import "#elements/router/RouterOutlet";
import { globalAK } from "#common/global";
@@ -13,16 +13,15 @@ import { AuthenticatedInterface } from "#elements/AuthenticatedInterface";
import { listen } from "#elements/decorators/listen";
import { WithBrandConfig } from "#elements/mixins/branding";
import { canAccessAdmin, WithSession } from "#elements/mixins/session";
import { ifPresent } from "#elements/utils/attributes";
import { ThemedImage } from "#elements/utils/images";
import { AKDrawerChangeEvent } from "#components/notifications/events";
import { AKDrawerChangeEvent } from "#elements/notifications/events";
import {
DrawerState,
persistDrawerParams,
readDrawerParams,
renderNotificationDrawerPanel,
} from "#components/notifications/utils";
} from "#elements/notifications/utils";
import { ifPresent } from "#elements/utils/attributes";
import { ThemedImage } from "#elements/utils/images";
import Styles from "#user/ak-interface-user.css";
import { ROUTES } from "#user/Routes";

View File

@@ -114,6 +114,10 @@ The worker status reporting change also uses one fewer PostgreSQL connection per
The Admin interface is also less resource-intensive in the browser due to lazy-loaded modals.
### Fewer packages, smaller attack surface
Weve removed 17 packages, trimming bloat and tightening security in one move. Fewer components mean fewer potential vulnerabilities, helping keep your authentik deployments faster, lighter, and more resilient.
### OAuth2 configurable grant types
[OAuth2 providers](../../add-secure-apps/providers/oauth2/index.mdx#oauth-20-flows-and-grant-types) now have a **Grant Types** setting that lets admins explicitly choose which grant types a given provider may use. The available options are Authorization Code, Implicit, Hybrid, Refresh token, Client credentials, Password, and Device-code. Existing providers default to having all grant types enabled to preserve current behavior, but you can now disable any grant types you don't want a particular client to use — useful for tightening security on individual integrations and disabling legacy flows like Implicit or Password where they aren't needed.

View File

@@ -8,11 +8,64 @@ Invitations are another way to create a user, by inviting someone to join your a
You can configure invitations either by:
- using [pre-built blueprints](#use-pre-built-blueprints-to-configure-invitations) (recommended for quick setup).
- using the [invitation wizard](#use-the-invitation-wizard) (recommended; creates the enrollment flow and the invitation in one guided process).
- using [pre-built blueprints](#use-pre-built-blueprints-to-configure-invitations) (good for showcasing multiple flow variations).
- [manually creating flows and stages](#manual-setup-without-blueprints) (for custom configurations).
:::info
You can also create a [policy](../../../customize/policies/) to see if the invitation was ever used.
You can also create a [policy](../../../customize/policies/) to check whether the invitation was ever used.
:::
## Use the invitation wizard
The invitation wizard, available from the **Directory** > **Invitations** page in the Admin interface, walks you through creating an invitation and (optionally) the enrollment flow it binds to in a single guided process.
### Step 1. Open the wizard
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Directory** > **Invitations**.
3. Click the caret (>) next to the **New Invitation** button and choose how the wizard should handle the invitation:
- **with Existing Enrollment Flow...**: bind the new invitation to an existing enrollment flow. Only enrollment flows that have an invitation stage bound to them are listed. This is also what the **New Invitation** button does by default.
- **with New Enrollment Flow and Invitation Stage...**: create a new minimal enrollment flow, including an invitation stage, then bind the invitation to it. Use this option when you do not yet have an enrollment flow set up, or when you want a separate enrollment flow for an invitation.
:::info Automatic flow selection
If you choose **with Existing Enrollment Flow...** and only one eligible flow exists, the wizard skips the flow selection step and takes you directly to the invitation details.
:::
### Step 2. Configure the enrollment flow
- If you picked an existing flow, select it from the **Enrollment flow** drop-down and click **Next**.
- If you are creating a new flow, fill in:
- **Flow name**: display name of the new enrollment flow.
- **Flow slug**: the slug for the flow which is included in the URL.
- **Invitation stage name**: name of the invitation stage that will be bound to the new flow.
- **User type**: the user type for users enrolled via this flow.
- **Continue flow without invitation**: when enabled, the flow proceeds to the next stage even when no invitation token is supplied. When disabled, the flow is cancelled if a valid invitation is not provided.
### Step 3. Configure the invitation details
- **Name**: provide a slug-style name for your invitation object (lowercase letters, numbers, and hyphens only).
- **Expires**: select a date and time for when the invitation should expire. Defaults to 48 hours from now.
- **Flow**: read-only; reflects the flow chosen in the previous step.
- **Custom attributes**: (_optional_) YAML or JSON that is loaded into the flow's `prompt_data` context to pre-fill user information. Field keys must match the keys configured in the flow's [prompt stage](../../add-secure-apps/flows-stages/stages/prompt/index.md). See the [example custom attributes](#step-3-create-the-invitation-object) below for sample payloads.
- **Single use**: when enabled, the invitation is deleted after the first successful enrollment.
Click **Next** to create the invitation. If you chose **with New Enrollment Flow and Invitation Stage...**, the supporting blueprint is imported at this point as well.
### Step 4. Share the invitation
After the invitation is created, the wizard's final step shows the **Link to use the invitation**. From there you can:
- Click **Copy Link** to copy the invitation URL to your clipboard.
- Click **Send via Email** to open the email step inside the wizard. Enter:
- **To**: one email per line, or comma/semicolon separated. Each recipient receives a separate email.
- **CC** / **BCC**: (_optional_) recipients for carbon and blind carbon copies.
- **Template**: the email template to use (the default `Invitation` template is recommended).
Click **Send** to queue the emails. They are sent asynchronously by the background worker. Check **System Tasks** for delivery status.
:::note Email configuration required
To send invitation emails, you must have configured email in authentik. Refer to the [Email configuration](../../install-config/email.mdx) documentation for details.
:::
## Use pre-built blueprints to configure invitations