diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py index ca6bb5c53059..65c3074489cc 100644 --- a/authentik/blueprints/api.py +++ b/authentik/blueprints/api.py @@ -3,8 +3,10 @@ from json import JSONDecodeError, loads from typing import cast +from django.apps import apps from django.conf import settings from django.core.files.uploadedfile import InMemoryUploadedFile +from django.db.models import Model from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema, inline_serializer from rest_framework.decorators import action @@ -23,7 +25,7 @@ from authentik.api.validation import validate from authentik.blueprints.models import BlueprintInstance -from authentik.blueprints.v1.common import Blueprint, EntryInvalidError +from authentik.blueprints.v1.common import Blueprint, BlueprintEntry, EntryInvalidError, YAMLTag from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict @@ -159,6 +161,28 @@ def check_blueprint_perms(blueprint: Blueprint, user: User, explicit_action: str ) } ) + check_secret_references(entry, apps.get_model(full_model), user) + + +def check_secret_references(entry: BlueprintEntry, model: type[Model], user: User): + """Attaching a secret can disclose it through the object, like in the API. + + Serializers only check this with a request, and blueprints are imported without one. + """ + from authentik.crypto.secrets.models import Secret + + for field in model._meta.fields: + if field.related_model is not Secret: + continue + value = entry.attrs.get(field.name) + # References to secrets created by the same blueprint need permission to create them. + if value is None or isinstance(value, YAMLTag): + continue + secret = Secret.objects.filter(pk=getattr(value, "pk", value)).first() + if secret and not user.has_perm("authentik_crypto_secrets.view_secret_value", secret): + raise PermissionDenied( + {entry.id: _("User lacks permission to use secret {secret}").format(secret=secret)} + ) class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet): diff --git a/authentik/core/api/used_by.py b/authentik/core/api/used_by.py index 9e4fbcc2b2a9..b409855d4cf4 100644 --- a/authentik/core/api/used_by.py +++ b/authentik/core/api/used_by.py @@ -4,7 +4,7 @@ from inspect import getmembers from django.db.models.base import Model -from django.db.models.deletion import SET_DEFAULT, SET_NULL +from django.db.models.deletion import PROTECT, SET_DEFAULT, SET_NULL from django.db.models.manager import Manager from drf_spectacular.utils import extend_schema from guardian.shortcuts import get_objects_for_user @@ -24,6 +24,7 @@ class DeleteAction(Enum): CASCADE_MANY = "cascade_many" SET_NULL = "set_null" SET_DEFAULT = "set_default" + PROTECT = "protect" LEFT_DANGLING = "left_dangling" @@ -44,6 +45,8 @@ def get_delete_action(manager: Manager) -> str: return DeleteAction.SET_NULL.value if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__: return DeleteAction.SET_DEFAULT.value + if manager.field.remote_field.on_delete.__name__ == PROTECT.__name__: + return DeleteAction.PROTECT.value if hasattr(manager, "source_field"): return DeleteAction.CASCADE_MANY.value return DeleteAction.CASCADE.value diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py index 04b3384e2fb6..3fe9fae55090 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -4,6 +4,7 @@ from django.db import models from django.db.models import Model +from django.utils.translation import gettext_lazy as _ from drf_spectacular.extensions import OpenApiSerializerFieldExtension from drf_spectacular.plumbing import build_basic_type from drf_spectacular.types import OpenApiTypes @@ -66,6 +67,23 @@ def to_representation(self, instance: Model): data.pop(field_name, None) return data + def to_internal_value(self, data): + from authentik.crypto.secrets.api import SecretReferenceField + + # Credentials moved from their own fields to secret references. Reject the old field + # names instead of ignoring them, so existing blueprints and API clients fail loudly. + # Remove with the legacy credential columns in 2027.2. + if hasattr(data, "keys"): + legacy = { + name: [_("Use %(field)s to reference a secret instead.") % {"field": f"{name}_ref"}] + for name in data.keys() + if name not in self.fields + and isinstance(self.fields.get(f"{name}_ref"), SecretReferenceField) + } + if legacy: + raise ValidationError(legacy) + return super().to_internal_value(data) + def update(self, instance: Model, validated_data): raise_errors_on_nested_writes("update", self, validated_data) info = model_meta.get_field_info(instance) @@ -107,7 +125,18 @@ def build_relational_field(self, field_name, relation_info): ): relation_info = relation_info._replace(has_through_model=False) - return super().build_relational_field(field_name, relation_info) + field_class, field_kwargs = super().build_relational_field(field_name, relation_info) + if relation_info.related_model._meta.label_lower == "authentik_crypto_secrets.secret": + from authentik.crypto.secrets.api import SecretReferenceField + + # The model field says whether a secret is required, optional, or generated when + # the object is created: generated references are blank, but not null. + model_field = relation_info.model_field + field_class = SecretReferenceField + field_kwargs["required"] = not model_field.blank + field_kwargs["allow_null"] = model_field.blank + field_kwargs["required_on_update"] = not model_field.null + return field_class, field_kwargs class PassiveSerializer(Serializer): diff --git a/authentik/core/tests/test_used_by.py b/authentik/core/tests/test_used_by.py new file mode 100644 index 000000000000..0e6247f1c7bd --- /dev/null +++ b/authentik/core/tests/test_used_by.py @@ -0,0 +1,27 @@ +"""Test used_by delete actions""" + +from django.urls import reverse +from rest_framework.test import APITestCase + +from authentik.core.api.used_by import DeleteAction +from authentik.core.tests.utils import create_test_admin_user +from authentik.lib.generators import generate_id +from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice + + +class TestUsedBy(APITestCase): + def test_protected_relation(self): + """Objects that block the deletion aren't reported as deleted with it""" + user = create_test_admin_user() + stage = AuthenticatorDuoStage.objects.create( + name=generate_id(), client_id=generate_id(), api_hostname=generate_id() + ) + DuoDevice.objects.create(user=user, stage=stage, duo_user_id=generate_id()) + self.client.force_login(user) + response = self.client.get( + reverse("authentik_api:authenticatorduostage-used-by", kwargs={"pk": stage.pk}) + ) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual( + [entry["action"] for entry in response.json()], [DeleteAction.PROTECT.value] + ) diff --git a/authentik/crypto/secrets/__init__.py b/authentik/crypto/secrets/__init__.py new file mode 100644 index 000000000000..8ca574c21fa0 --- /dev/null +++ b/authentik/crypto/secrets/__init__.py @@ -0,0 +1 @@ +"""Managed secrets.""" diff --git a/authentik/crypto/secrets/api.py b/authentik/crypto/secrets/api.py new file mode 100644 index 000000000000..138cb967050f --- /dev/null +++ b/authentik/crypto/secrets/api.py @@ -0,0 +1,202 @@ +"""Managed secrets API.""" + +from django.core.exceptions import ValidationError as DjangoValidationError +from django.db import transaction +from django.db.models.deletion import ProtectedError +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied, ValidationError +from rest_framework.fields import CharField, IntegerField, SkipField, empty +from rest_framework.permissions import IsAuthenticated +from rest_framework.relations import PrimaryKeyRelatedField +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + +from authentik.blueprints.api import ManagedSerializer +from authentik.core.api.used_by import UsedByMixin +from authentik.core.api.utils import ModelSerializer, PassiveSerializer +from authentik.crypto.secrets.models import Secret, SecretType, generate_secret_value +from authentik.events.models import Event, EventAction +from authentik.rbac.decorators import permission_required + +# Long enough for any credential, short enough not to be abused to store large values. +MAX_GENERATED_LENGTH = 1024 + + +class SecretReferenceField(PrimaryKeyRelatedField): + """Attaching a credential can disclose it through the consumer.""" + + def __init__(self, allowed_types=(SecretType.TEXT,), required_on_update=False, **kwargs): + self.allowed_types = allowed_types + self.required_on_update = required_on_update + super().__init__(**kwargs) + + def run_validation(self, data=empty): + # Objects that generate their secret may omit it when created, but must keep one. + if data in (None, "") and self.required_on_update and self.parent.instance: + raise ValidationError(_("A secret is required.")) + return super().run_validation(data) + + def to_internal_value(self, data): + secret = super().to_internal_value(data) + request = self.context.get("request") + instance = self.parent.instance + unchanged = instance and getattr(instance, f"{self.source}_id") == secret.pk + if ( + request + and not unchanged + and not request.user.has_perm("authentik_crypto_secrets.view_secret_value", secret) + ): + raise PermissionDenied(_("You do not have permission to use this secret.")) + if secret.type not in self.allowed_types: + raise ValidationError(_("This secret type is not supported by this field.")) + return secret + + +class SecretSerializer(ManagedSerializer, ModelSerializer): + """Create and configure a secret without exposing its value.""" + + value = CharField( + source="secret_value", + write_only=True, + required=False, + allow_blank=True, + trim_whitespace=False, + ) + length = IntegerField( + write_only=True, + required=False, + min_value=1, + max_value=MAX_GENERATED_LENGTH, + help_text=_( + "Length of the generated value when creating a text secret without a value. " + "Defaults to the default token length." + ), + ) + + def validate_value(self, value: str) -> str: + if value == "": + raise SkipField + instance = self.instance + if not instance: + return value + request = self.context.get("request") + if request and not request.user.has_perm( + "authentik_crypto_secrets.rotate_secret", instance + ): + raise PermissionDenied(_("You do not have permission to replace this value.")) + return value + + def validate(self, attrs: dict) -> dict: + instance = self.instance + if instance and attrs.get("type", instance.type) != instance.type: + raise ValidationError({"type": _("Type cannot be changed after creation.")}) + secret_type = attrs.get("type", instance.type if instance else SecretType.TEXT) + if not instance and secret_type != SecretType.TEXT and not attrs.get("secret_value"): + raise ValidationError({"value": _("A value is required for this type.")}) + if instance: + # Re-applying a blueprint passes the length again, but it only applies when + # the secret is created. + attrs.pop("length", None) + elif "length" in attrs and (secret_type != SecretType.TEXT or attrs.get("secret_value")): + raise ValidationError( + {"length": _("Length only applies when generating a new text secret.")} + ) + if "secret_value" in attrs: + try: + (instance or Secret(type=secret_type)).validate_value(attrs["secret_value"]) + except DjangoValidationError as exc: + raise ValidationError({"value": exc.messages}) from exc + return attrs + + def create(self, validated_data: dict) -> Secret: + if length := validated_data.pop("length", None): + validated_data["secret_value"] = generate_secret_value(length) + return super().create(validated_data) + + def update(self, instance: Secret, validated_data: dict) -> Secret: + value = validated_data.pop("secret_value", None) + with transaction.atomic(): + if validated_data: + for field, field_value in validated_data.items(): + setattr(instance, field, field_value) + instance.save(update_fields=[*validated_data, "last_updated"]) + if value is not None: + instance.replace_value(value, self.context.get("request")) + return instance + + class Meta: + model = Secret + fields = ["pk", "name", "type", "managed", "value", "length", "created", "last_updated"] + extra_kwargs = { + "managed": {"read_only": True}, + "created": {"read_only": True}, + "last_updated": {"read_only": True}, + } + + +class SecretValueSerializer(PassiveSerializer): + """A secret value.""" + + value = CharField(read_only=True) + + +class RotatedSecretSerializer(PassiveSerializer): + """A rotated value, hidden when the caller cannot view it.""" + + value = CharField(read_only=True, allow_null=True) + + +class SecretViewSet(UsedByMixin, ModelViewSet): + """Create and manage named credentials referenced by authentik configuration objects. + + List and detail responses contain metadata only. Reading a value requires the + view_value action and its separate permission. Replacing or manually rotating + a value requires rotate_secret permission and records an audit event. + Rotation changes the value stored in authentik; it does not update external systems. + """ + + queryset = Secret.objects.all() + serializer_class = SecretSerializer + ordering = ["name"] + search_fields = ["name"] + filterset_fields = {"name": ["exact"], "type": ["exact", "in"], "managed": ["exact"]} + + def destroy(self, request: Request, *args, **kwargs) -> Response: + try: + return super().destroy(request, *args, **kwargs) + except ProtectedError: + raise ValidationError( + _("Secret is in use. Remove it from the objects referencing it first.") + ) from None + + @permission_required("authentik_crypto_secrets.view_secret_value") + @extend_schema(responses={200: SecretValueSerializer}) + @action(detail=True, pagination_class=None, permission_classes=[IsAuthenticated]) + def view_value(self, request: Request, pk: str) -> Response: + """Return and audit a secret value.""" + secret = self.get_object() + Event.new(EventAction.SECRET_VIEW, secret=secret).from_http(request) + return Response(SecretValueSerializer({"value": secret.secret_value}).data) + + @permission_required("authentik_crypto_secrets.rotate_secret") + @extend_schema(request=None, responses={200: RotatedSecretSerializer}) + @action( + detail=True, + methods=["POST"], + pagination_class=None, + permission_classes=[IsAuthenticated], + ) + def rotate(self, request: Request, pk: str) -> Response: + """Replace a text secret with a generated value.""" + secret = self.get_object() + if secret.type != SecretType.TEXT: + raise ValidationError({"non_field_errors": [_("Only text secrets can be rotated.")]}) + try: + value = secret.rotate(request) + except DjangoValidationError as exc: + raise ValidationError({"non_field_errors": exc.messages}) from exc + can_view = request.user.has_perm("authentik_crypto_secrets.view_secret_value", secret) + return Response(RotatedSecretSerializer({"value": value if can_view else None}).data) diff --git a/authentik/crypto/secrets/apps.py b/authentik/crypto/secrets/apps.py new file mode 100644 index 000000000000..a124db26e0f0 --- /dev/null +++ b/authentik/crypto/secrets/apps.py @@ -0,0 +1,12 @@ +"""Managed secrets app configuration.""" + +from authentik.blueprints.apps import ManagedAppConfig + + +class AuthentikSecretsConfig(ManagedAppConfig): + """Managed secrets app configuration.""" + + name = "authentik.crypto.secrets" + label = "authentik_crypto_secrets" + verbose_name = "authentik Secrets" + default = True diff --git a/authentik/crypto/secrets/migrations/0001_initial.py b/authentik/crypto/secrets/migrations/0001_initial.py new file mode 100644 index 000000000000..68e5dec34d11 --- /dev/null +++ b/authentik/crypto/secrets/migrations/0001_initial.py @@ -0,0 +1,58 @@ +# Generated by Django 5.2.17 on 2026-10-10 19:15 + +import authentik.crypto.secrets.models +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Secret", + 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", + ), + ), + ( + "secret_uuid", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("name", models.TextField(unique=True)), + ( + "type", + models.TextField( + choices=[("text", "Text"), ("json", "JSON"), ("file", "File")], + default="text", + ), + ), + ( + "secret_value", + models.TextField(default=authentik.crypto.secrets.models.generate_secret_value), + ), + ], + options={ + "verbose_name": "Secret", + "verbose_name_plural": "Secrets", + "permissions": [ + ("view_secret_value", "View secret's value"), + ("rotate_secret", "Rotate secret's value"), + ], + }, + ), + ] diff --git a/authentik/crypto/secrets/migrations/__init__.py b/authentik/crypto/secrets/migrations/__init__.py new file mode 100644 index 000000000000..9f81872da17e --- /dev/null +++ b/authentik/crypto/secrets/migrations/__init__.py @@ -0,0 +1 @@ +"""Managed secrets migrations.""" diff --git a/authentik/crypto/secrets/models.py b/authentik/crypto/secrets/models.py new file mode 100644 index 000000000000..5da226bc941d --- /dev/null +++ b/authentik/crypto/secrets/models.py @@ -0,0 +1,159 @@ +"""Managed secret models.""" + +from base64 import b64decode +from json import JSONDecodeError, dumps, loads +from typing import TYPE_CHECKING +from uuid import uuid4 + +from django.core.exceptions import ValidationError +from django.db import IntegrityError, models, transaction +from django.utils.translation import gettext_lazy as _ +from yaml import YAMLError, safe_load + +from authentik.admin.utils import get_system_settings +from authentik.blueprints.models import ManagedModel +from authentik.core.models import default_token_key +from authentik.crypto.secrets.signals import secret_value_changed, secret_value_validating +from authentik.events.middleware import audit_ignore +from authentik.events.models import Event, EventAction +from authentik.lib.generators import generate_id +from authentik.lib.models import CreatedUpdatedModel, SerializerModel + +if TYPE_CHECKING: + from rest_framework.request import Request + from rest_framework.serializers import Serializer + + +class SecretType(models.TextChoices): + """What a secret value contains. + + Only text values can be generated and rotated. JSON and file values are + issued by another system and must be supplied by the administrator. + """ + + TEXT = "text", _("Text") + JSON = "json", _("JSON") + FILE = "file", _("File") + + +def generate_secret_value(length: int | None = None) -> str: + """Generate a text value, by default with the configured default token length. + + Values only use ASCII letters and digits, which every consumer accepts: OAuth2 requires + VSCHAR client secrets, and HTTP Basic authentication splits credentials on colons. + """ + return generate_id(length) if length else default_token_key() + + +def parse_json(value: str) -> dict: + """Parse a JSON object, accepting YAML syntax for files such as kubeconfigs. + + The parsed object must also be serializable as JSON, since consumers and + downgrade migrations store it in JSON fields. YAML dates are rejected. + """ + try: + data = loads(value) + except JSONDecodeError: + try: + data = safe_load(value) + except YAMLError: + # The parser error quotes the input, which is the credential. + raise ValueError("Value is not valid JSON or YAML") from None + if not isinstance(data, dict): + raise ValueError("Value must be a JSON or YAML object") + try: + dumps(data) + except TypeError: + raise ValueError("Value must only contain JSON types") from None + return data + + +class Secret(SerializerModel, ManagedModel, CreatedUpdatedModel): + """A named value that can be shared by configuration objects.""" + + secret_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) + name = models.TextField(unique=True) + type = models.TextField(choices=SecretType.choices, default=SecretType.TEXT) + # Named so that event diffs hide it, like other credential fields. + secret_value = models.TextField(default=generate_secret_value) + + def get_json(self) -> dict: + """Read the object stored in a JSON secret.""" + if self.type != SecretType.JSON: + raise ValueError("Secret is not a JSON secret") + return parse_json(self.secret_value) + + def validate_value(self, value: str) -> None: + """Validate a value against this secret's type.""" + try: + if self.type == SecretType.JSON: + parse_json(value) + elif self.type == SecretType.FILE: + b64decode(value, validate=True) + except ValueError as exc: + raise ValidationError( + _("Value must be base64-encoded.") + if self.type == SecretType.FILE + else _("Value must be a JSON or YAML object.") + ) from exc + if not self._state.adding: + secret_value_validating.send(sender=Secret, secret=self, value=value) + + def replace_value(self, value: str, request: Request | None = None) -> None: + """Replace and audit the value, then signal consumers.""" + if value == self.secret_value: + return + self.validate_value(value) + with transaction.atomic(): + self.secret_value = value + with audit_ignore(): + self.save(update_fields=["secret_value", "last_updated"]) + event = Event.new(EventAction.SECRET_ROTATE, secret=self) + if request: + event.from_http(request) + else: + event.save() + secret_value_changed.send(sender=Secret, secret=self) + + def rotate(self, request: Request | None = None) -> str: + """Generate and store a new text value. + + The new value is at least as long as the current one, so a secret generated with a + consumer's own length, such as a 128 character OAuth2 client secret, keeps it. + """ + if self.type != SecretType.TEXT: + raise ValueError("Only text secrets can be rotated.") + value = generate_secret_value( + max(len(self.secret_value), get_system_settings().default_token_length) + ) + self.replace_value(value, request) + return value + + @property + def serializer(self) -> type[Serializer]: + from authentik.crypto.secrets.api import SecretSerializer + + return SecretSerializer + + def __str__(self) -> str: + return self.name + + class Meta: + verbose_name = _("Secret") + verbose_name_plural = _("Secrets") + permissions = [ + ("view_secret_value", _("View secret's value")), + ("rotate_secret", _("Rotate secret's value")), + ] + + +def create_named_secret(name: str, **fields) -> Secret: + """Create a secret with a readable, collision-safe name.""" + for suffix in range(1, 100): + candidate = name if suffix == 1 else f"{name} ({suffix})" + try: + with transaction.atomic(): + return Secret.objects.create(name=candidate, **fields) + except IntegrityError: + continue + raise IntegrityError(f"Could not allocate a name for {name!r}") diff --git a/authentik/crypto/secrets/signals.py b/authentik/crypto/secrets/signals.py new file mode 100644 index 000000000000..e74be4bfab52 --- /dev/null +++ b/authentik/crypto/secrets/signals.py @@ -0,0 +1,9 @@ +"""Managed secret signals.""" + +from django.dispatch import Signal + +# Sent with `secret` and the proposed `value` before an existing secret's value changes. +# Receivers raise ValidationError when a consumer can't use the value. +secret_value_validating = Signal() +# Sent with `secret` after its value changed, inside the transaction. +secret_value_changed = Signal() diff --git a/authentik/crypto/secrets/tests/__init__.py b/authentik/crypto/secrets/tests/__init__.py new file mode 100644 index 000000000000..9cb8dbf1d8c5 --- /dev/null +++ b/authentik/crypto/secrets/tests/__init__.py @@ -0,0 +1 @@ +"""Managed secrets tests.""" diff --git a/authentik/crypto/secrets/tests/test_api.py b/authentik/crypto/secrets/tests/test_api.py new file mode 100644 index 000000000000..1be778032af0 --- /dev/null +++ b/authentik/crypto/secrets/tests/test_api.py @@ -0,0 +1,267 @@ +"""Managed secret API tests.""" + +from unittest.mock import PropertyMock, patch + +from django.urls import reverse +from rest_framework.test import APITestCase + +from authentik.core.tests.utils import create_test_admin_user, create_test_user +from authentik.crypto.secrets.api import SecretSerializer +from authentik.crypto.secrets.models import Secret, SecretType +from authentik.events.models import Event, EventAction + + +class TestSecretsAPI(APITestCase): + """Secret values require permissions separate from metadata.""" + + def setUp(self) -> None: + self.admin = create_test_admin_user() + self.user = create_test_user() + self.secret = Secret.objects.create(name="test") + + def test_create_generated(self): + self.client.force_login(self.admin) + response = self.client.post(reverse("authentik_api:secret-list"), {"name": "created"}) + self.assertEqual(response.status_code, 201, response.content) + secret = Secret.objects.get(name="created") + self.assertTrue(secret.secret_value) + self.assertNotIn(secret.secret_value, response.content.decode()) + + def test_create_generated_with_length(self): + self.client.force_login(self.admin) + list_url = reverse("authentik_api:secret-list") + response = self.client.post(list_url, {"name": "long", "length": 128}) + self.assertEqual(response.status_code, 201, response.content) + self.assertEqual(len(Secret.objects.get(name="long").secret_value), 128) + for data in [ + {"name": "given", "length": 128, "value": "given"}, + {"name": "json", "length": 128, "type": "json", "value": "{}"}, + ]: + with self.subTest(data=data): + response = self.client.post(list_url, data) + self.assertEqual(response.status_code, 400, response.content) + self.assertIn("length", response.json()) + + def test_length_is_ignored_on_update(self): + """Re-applying a blueprint that sets a length doesn't fail or change the value.""" + previous = self.secret.secret_value + serializer = SecretSerializer( + instance=self.secret, data={"name": "test", "length": 128}, partial=True + ) + self.assertTrue(serializer.is_valid(), serializer.errors) + serializer.save() + self.secret.refresh_from_db() + self.assertEqual(self.secret.secret_value, previous) + + def test_value_whitespace_is_preserved(self): + self.client.force_login(self.admin) + value = " -----BEGIN KEY-----\nexact credential\n" + response = self.client.post( + reverse("authentik_api:secret-list"), {"name": "whitespace", "value": value} + ) + self.assertEqual(response.status_code, 201, response.content) + secret = Secret.objects.get(pk=response.json()["pk"]) + self.assertEqual(secret.secret_value, value) + + response = self.client.patch( + reverse("authentik_api:secret-detail", kwargs={"pk": secret.pk}), + {"value": " replacement "}, + ) + self.assertEqual(response.status_code, 200, response.content) + secret.refresh_from_db() + self.assertEqual(secret.secret_value, " replacement ") + + def test_view_value_permission_and_audit(self): + self.user.assign_perms_to_managed_role("authentik_crypto_secrets.view_secret", self.secret) + self.client.force_login(self.user) + url = reverse("authentik_api:secret-view-value", kwargs={"pk": self.secret.pk}) + self.assertEqual(self.client.get(url).status_code, 403) + + self.user.assign_perms_to_managed_role( + "authentik_crypto_secrets.view_secret_value", self.secret + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"value": self.secret.secret_value}) + self.assertTrue(Event.objects.filter(action=EventAction.SECRET_VIEW).exists()) + + def test_rotation_requires_permission_and_returns_value_only_when_allowed(self): + self.user.assign_perms_to_managed_role("authentik_crypto_secrets.view_secret", self.secret) + self.user.assign_perms_to_managed_role( + "authentik_crypto_secrets.view_secret_value", self.secret + ) + self.client.force_login(self.user) + url = reverse("authentik_api:secret-rotate", kwargs={"pk": self.secret.pk}) + previous = self.secret.secret_value + self.assertEqual(self.client.post(url).status_code, 403) + self.secret.refresh_from_db() + self.assertEqual(self.secret.secret_value, previous) + + self.user.assign_perms_to_managed_role( + "authentik_crypto_secrets.rotate_secret", self.secret + ) + response = self.client.post(url) + self.assertEqual(response.status_code, 200) + self.secret.refresh_from_db() + self.assertEqual(response.json()["value"], self.secret.secret_value) + self.assertNotEqual(self.secret.secret_value, previous) + + def test_global_permissions_allow_replacement_and_rotation_disclosure(self): + self.user.assign_perms_to_managed_role( + [ + "authentik_crypto_secrets.view_secret", + "authentik_crypto_secrets.change_secret", + "authentik_crypto_secrets.rotate_secret", + "authentik_crypto_secrets.view_secret_value", + ] + ) + self.client.force_login(self.user) + response = self.client.patch( + reverse("authentik_api:secret-detail", kwargs={"pk": self.secret.pk}), + {"value": "replacement"}, + ) + self.assertEqual(response.status_code, 200, response.content) + self.secret.refresh_from_db() + self.assertEqual(self.secret.secret_value, "replacement") + response = self.client.post( + reverse("authentik_api:secret-rotate", kwargs={"pk": self.secret.pk}) + ) + self.assertEqual(response.status_code, 200, response.content) + self.secret.refresh_from_db() + self.assertEqual(response.json()["value"], self.secret.secret_value) + + def test_type_cannot_change_after_creation(self): + self.client.force_login(self.admin) + response = self.client.patch( + reverse("authentik_api:secret-detail", kwargs={"pk": self.secret.pk}), + {"type": SecretType.FILE, "value": "aGk="}, + ) + self.assertEqual(response.status_code, 400) + self.assertIn("type", response.json()) + self.secret.refresh_from_db() + self.assertEqual(self.secret.type, SecretType.TEXT) + + def test_replace_value_requires_rotate_permission(self): + self.user.assign_perms_to_managed_role("authentik_crypto_secrets.view_secret", self.secret) + self.user.assign_perms_to_managed_role( + "authentik_crypto_secrets.change_secret", self.secret + ) + self.client.force_login(self.user) + url = reverse("authentik_api:secret-detail", kwargs={"pk": self.secret.pk}) + response = self.client.patch(url, {"value": "replacement"}) + self.assertEqual(response.status_code, 403) + self.secret.refresh_from_db() + self.assertNotEqual(self.secret.secret_value, "replacement") + + self.user.assign_perms_to_managed_role( + "authentik_crypto_secrets.rotate_secret", self.secret + ) + response = self.client.patch(url, {"value": "replacement"}) + self.assertEqual(response.status_code, 200, response.content) + self.secret.refresh_from_db() + self.assertEqual(self.secret.secret_value, "replacement") + + def test_file_validation_and_rotation(self): + self.client.force_login(self.admin) + list_url = reverse("authentik_api:secret-list") + response = self.client.post(list_url, {"name": "file", "type": "file"}) + self.assertEqual(response.status_code, 400) + + response = self.client.post( + list_url, {"name": "file", "type": "file", "value": "not base64"} + ) + self.assertEqual(response.status_code, 400) + secret = Secret.objects.create(name="file", type=SecretType.FILE, secret_value="aGk=") + response = self.client.patch( + reverse("authentik_api:secret-detail", kwargs={"pk": secret.pk}), + {"value": "not base64"}, + ) + self.assertEqual(response.status_code, 400) + secret.refresh_from_db() + self.assertEqual(secret.secret_value, "aGk=") + response = self.client.post( + reverse("authentik_api:secret-rotate", kwargs={"pk": secret.pk}) + ) + self.assertEqual(response.status_code, 400) + + def test_json_validation(self): + self.client.force_login(self.admin) + list_url = reverse("authentik_api:secret-list") + for value in ["[]", "plain", "{broken", "date: 2026-01-01"]: + with self.subTest(value=value): + response = self.client.post( + list_url, {"name": "json", "type": "json", "value": value} + ) + self.assertEqual(response.status_code, 400, response.content) + self.assertNotIn(value, response.content.decode()) + response = self.client.post( + list_url, {"name": "json", "type": "json", "value": "token: value\n"} + ) + self.assertEqual(response.status_code, 201, response.content) + secret = Secret.objects.get(name="json") + self.assertEqual(secret.get_json(), {"token": "value"}) + response = self.client.post( + reverse("authentik_api:secret-rotate", kwargs={"pk": secret.pk}) + ) + self.assertEqual(response.status_code, 400) + + def test_filter_compatible_types(self): + self.client.force_login(self.admin) + Secret.objects.create(name="json", type=SecretType.JSON, secret_value="{}") + Secret.objects.create(name="file", type=SecretType.FILE, secret_value="aGk=") + response = self.client.get(reverse("authentik_api:secret-list"), {"type__in": "json,file"}) + self.assertEqual(response.status_code, 200) + self.assertCountEqual( + [secret["type"] for secret in response.json()["results"]], ["json", "file"] + ) + + def test_blank_value_keeps_existing_value_without_rotate_permission(self): + self.user.assign_perms_to_managed_role("authentik_crypto_secrets.view_secret", self.secret) + self.user.assign_perms_to_managed_role( + "authentik_crypto_secrets.change_secret", self.secret + ) + self.client.force_login(self.user) + previous = self.secret.secret_value + + response = self.client.patch( + reverse("authentik_api:secret-detail", kwargs={"pk": self.secret.pk}), + {"name": "renamed", "value": ""}, + ) + + self.assertEqual(response.status_code, 200, response.content) + self.secret.refresh_from_db() + self.assertEqual(self.secret.name, "renamed") + self.assertEqual(self.secret.secret_value, previous) + self.assertFalse(Event.objects.filter(action=EventAction.SECRET_ROTATE).exists()) + + def test_failed_replacement_rolls_back_metadata(self): + serializer = SecretSerializer( + instance=self.secret, data={"name": "renamed", "value": "replacement"}, partial=True + ) + self.assertTrue(serializer.is_valid(), serializer.errors) + with ( + patch( + "authentik.crypto.secrets.models.secret_value_changed.send", + side_effect=RuntimeError("consumer failed"), + ), + self.assertRaises(RuntimeError), + ): + serializer.save() + self.secret.refresh_from_db() + self.assertEqual(self.secret.name, "test") + self.assertNotEqual(self.secret.secret_value, "replacement") + self.assertFalse(Event.objects.filter(action=EventAction.SECRET_ROTATE).exists()) + + @patch( + "authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled", + PropertyMock(return_value=True), + ) + def test_audit_diff_hides_value(self): + self.client.force_login(self.admin) + response = self.client.post( + reverse("authentik_api:secret-list"), {"name": "audited", "value": "audited-value"} + ) + self.assertEqual(response.status_code, 201, response.content) + event = Event.objects.get(action=EventAction.MODEL_CREATED, context__model__name="audited") + self.assertIn("secret_value", event.context["diff"]) + self.assertNotIn("audited-value", str(event.context)) diff --git a/authentik/crypto/secrets/tests/test_fields.py b/authentik/crypto/secrets/tests/test_fields.py new file mode 100644 index 000000000000..e8dff65c096c --- /dev/null +++ b/authentik/crypto/secrets/tests/test_fields.py @@ -0,0 +1,45 @@ +"""Consumer references accept only compatible secret types.""" + +from base64 import b64encode + +from django.test import TestCase +from rest_framework.exceptions import ValidationError +from rest_framework.serializers import Serializer + +from authentik.crypto.secrets.api import SecretReferenceField +from authentik.crypto.secrets.models import Secret, SecretType + + +class TestSecretReferenceTypes(TestCase): + def test_reference_types(self): + for field, allowed in [ + (SecretReferenceField(queryset=Secret.objects.all()), {SecretType.TEXT}), + ( + SecretReferenceField( + queryset=Secret.objects.all(), allowed_types=(SecretType.JSON,) + ), + {SecretType.JSON}, + ), + ( + SecretReferenceField( + queryset=Secret.objects.all(), + allowed_types=(SecretType.TEXT, SecretType.FILE), + ), + {SecretType.TEXT, SecretType.FILE}, + ), + ]: + field.bind("secret", Serializer()) + for secret_type in SecretType: + with self.subTest(field=type(field).__name__, type=secret_type, allowed=allowed): + secret = Secret.objects.create( + name=f"{Secret.objects.count()}", + type=secret_type, + secret_value=( + b64encode(b"{}").decode() if secret_type == SecretType.FILE else "{}" + ), + ) + if secret_type in allowed: + self.assertEqual(field.run_validation(str(secret.pk)), secret) + else: + with self.assertRaises(ValidationError): + field.run_validation(str(secret.pk)) diff --git a/authentik/crypto/secrets/tests/test_models.py b/authentik/crypto/secrets/tests/test_models.py new file mode 100644 index 000000000000..775eb398701e --- /dev/null +++ b/authentik/crypto/secrets/tests/test_models.py @@ -0,0 +1,121 @@ +"""Managed secret model tests.""" + +from traceback import format_exception +from unittest.mock import patch + +from django.core.exceptions import ValidationError +from django.test import TestCase + +from authentik.admin.models import DEFAULT_TOKEN_LENGTH +from authentik.admin.utils import get_system_settings +from authentik.crypto.secrets.models import Secret, SecretType, create_named_secret +from authentik.events.models import Event, EventAction + + +class TestSecret(TestCase): + """Managed secret behavior.""" + + def test_rotate(self): + secret = Secret.objects.create(name="test") + previous = secret.secret_value + + value = secret.rotate() + + secret.refresh_from_db() + self.assertEqual(secret.secret_value, value) + self.assertNotEqual(value, previous) + self.assertEqual(len(value), DEFAULT_TOKEN_LENGTH) + event = Event.objects.get(action=EventAction.SECRET_ROTATE) + self.assertEqual(event.context["secret"]["pk"], secret.pk.hex) + self.assertNotIn(value, str(event.context)) + + def test_generation_uses_configured_token_length(self): + settings = get_system_settings() + settings.default_token_length = 64 + settings.save() + secret = Secret.objects.create(name="configured") + self.assertEqual(len(secret.secret_value), 64) + self.assertRegex(secret.secret_value, r"^[a-zA-Z0-9]+$") + + settings.default_token_length = 80 + settings.save() + value = secret.rotate() + self.assertEqual(len(value), 80) + self.assertRegex(value, r"^[a-zA-Z0-9]+$") + + def test_rotation_keeps_length(self): + """Rotating keeps a consumer's longer length, but never goes below the default.""" + for current, expected in [("x" * 128, 128), ("short", DEFAULT_TOKEN_LENGTH)]: + with self.subTest(length=len(current)): + secret = Secret.objects.create(name=f"length-{len(current)}", secret_value=current) + self.assertEqual(len(secret.rotate()), expected) + + def test_non_text_cannot_rotate(self): + secret = Secret.objects.create(name="file", type=SecretType.FILE, secret_value="aGk=") + with self.assertRaises(ValueError): + secret.rotate() + + def test_collision_safe_name(self): + first = create_named_secret("consumer") + second = create_named_secret("consumer") + self.assertEqual(first.name, "consumer") + self.assertEqual(second.name, "consumer (2)") + + def test_json_values(self): + for value in ['{"token": "value"}', "token: value\n"]: + with self.subTest(value=value): + secret = Secret(type=SecretType.JSON, secret_value=value) + secret.validate_value(value) + self.assertEqual(secret.get_json(), {"token": "value"}) + + def test_invalid_values(self): + for secret_type, value in [ + (SecretType.JSON, "[]"), + (SecretType.JSON, "null"), + (SecretType.JSON, "{broken"), + (SecretType.JSON, "date: 2026-01-01"), + (SecretType.FILE, "not base64"), + ]: + with self.subTest(type=secret_type, value=value): + with self.assertRaises(ValidationError): + Secret(type=secret_type).validate_value(value) + + def test_only_json_secrets_are_parsed(self): + with self.assertRaises(ValueError): + Secret(type=SecretType.TEXT, secret_value="{}").get_json() + + def test_replacing_unchanged_value_does_not_audit_rotation(self): + secret = Secret.objects.create(name="unchanged", secret_value="current") + secret.replace_value("current") + self.assertFalse(Event.objects.filter(action=EventAction.SECRET_ROTATE).exists()) + + def test_parser_error_does_not_disclose_value(self): + secret = Secret(type=SecretType.JSON, secret_value="private-credential: [unterminated") + with self.assertRaises(ValueError) as error: + secret.get_json() + self.assertNotIn("private-credential", "".join(format_exception(error.exception))) + + def test_invalid_file_replacement_preserves_value(self): + secret = Secret.objects.create(name="file", type=SecretType.FILE, secret_value="aGk=") + with self.assertRaises(ValidationError): + secret.replace_value("not base64") + self.assertEqual(secret.secret_value, "aGk=") + secret.refresh_from_db() + self.assertEqual(secret.secret_value, "aGk=") + self.assertFalse(Event.objects.filter(action=EventAction.SECRET_ROTATE).exists()) + + def test_failed_rotation_keeps_value(self): + secret = Secret.objects.create(name="rollback", secret_value="old") + previous_updated = secret.last_updated + with ( + patch( + "authentik.crypto.secrets.models.secret_value_changed.send", + side_effect=RuntimeError("consumer failed"), + ), + self.assertRaises(RuntimeError), + ): + secret.rotate() + secret.refresh_from_db() + self.assertEqual(secret.secret_value, "old") + self.assertEqual(secret.last_updated, previous_updated) + self.assertFalse(Event.objects.filter(action=EventAction.SECRET_ROTATE).exists()) diff --git a/authentik/crypto/secrets/tests/utils.py b/authentik/crypto/secrets/tests/utils.py new file mode 100644 index 000000000000..481205109fdd --- /dev/null +++ b/authentik/crypto/secrets/tests/utils.py @@ -0,0 +1,9 @@ +"""Helpers for models that reference secrets in tests.""" + +from authentik.crypto.secrets.models import Secret, SecretType +from authentik.lib.generators import generate_id + + +def create_test_secret(value: str, type: SecretType = SecretType.TEXT) -> Secret: + """Create a uniquely named secret with a known value.""" + return Secret.objects.create(name=generate_id(), type=type, secret_value=value) diff --git a/authentik/crypto/secrets/urls.py b/authentik/crypto/secrets/urls.py new file mode 100644 index 000000000000..2c407edbb734 --- /dev/null +++ b/authentik/crypto/secrets/urls.py @@ -0,0 +1,5 @@ +"""Managed secrets API URLs.""" + +from authentik.crypto.secrets.api import SecretViewSet + +api_urlpatterns = [("secrets/secrets", SecretViewSet)] diff --git a/authentik/events/tests/test_models.py b/authentik/events/tests/test_models.py index 98539a7fca1f..c47e843198d0 100644 --- a/authentik/events/tests/test_models.py +++ b/authentik/events/tests/test_models.py @@ -6,6 +6,7 @@ from django.test import TestCase from authentik.core.models import default_token_key +from authentik.crypto.secrets.models import generate_secret_value from authentik.events.models import default_event_duration from authentik.lib.utils.reflection import get_apps @@ -21,7 +22,7 @@ def tester(self: TestModels): allowed = 0 # Token-like objects need to lookup the current tenant to get the default token length for field in test_model._meta.fields: - if field.default in [default_token_key, default_event_duration]: + if field.default in [default_token_key, default_event_duration, generate_secret_value]: allowed += 1 with self.assertNumQueries(allowed): str(test_model()) diff --git a/authentik/root/settings.py b/authentik/root/settings.py index a691c61d28c5..7656cef5db79 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -82,6 +82,7 @@ "authentik.api", "authentik.core", "authentik.crypto", + "authentik.crypto.secrets", "authentik.endpoints", "authentik.endpoints.connectors.agent", "authentik.events", @@ -202,6 +203,7 @@ "SAMLLogoutMethods": "authentik.providers.saml.models.SAMLLogoutMethods", "SAMLNameIDPolicyEnum": "authentik.sources.saml.models.SAMLNameIDPolicy", "SCIMAuthenticationModeEnum": "authentik.providers.scim.models.SCIMAuthenticationMode", + "SecretTypeEnum": "authentik.crypto.secrets.models.SecretType", "StageModeEnum": "authentik.endpoints.models.StageMode", "TaskAggregatedStatusEnum": "authentik.tasks.models.TaskStatus", "TaskStatusEnum": "django_dramatiq_postgres.models.TaskState", diff --git a/blueprints/schema.json b/blueprints/schema.json index 963ad1c80d56..37f916b05da8 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -496,6 +496,46 @@ } } }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_crypto_secrets.secret" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "created", + "must_created", + "present" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "permissions": { + "$ref": "#/definitions/model_authentik_crypto_secrets.secret_permissions" + }, + "attrs": { + "$ref": "#/definitions/model_authentik_crypto_secrets.secret" + }, + "identifiers": { + "$ref": "#/definitions/model_authentik_crypto_secrets.secret" + } + } + }, { "type": "object", "required": [ @@ -5703,6 +5743,12 @@ "authentik_crypto.view_certificatekeypair", "authentik_crypto.view_certificatekeypair_certificate", "authentik_crypto.view_certificatekeypair_key", + "authentik_crypto_secrets.add_secret", + "authentik_crypto_secrets.change_secret", + "authentik_crypto_secrets.delete_secret", + "authentik_crypto_secrets.rotate_secret", + "authentik_crypto_secrets.view_secret", + "authentik_crypto_secrets.view_secret_value", "authentik_endpoints.add_connector", "authentik_endpoints.add_device", "authentik_endpoints.add_deviceaccessgroup", @@ -7097,6 +7143,12 @@ "authentik_crypto.view_certificatekeypair", "authentik_crypto.view_certificatekeypair_certificate", "authentik_crypto.view_certificatekeypair_key", + "authentik_crypto_secrets.add_secret", + "authentik_crypto_secrets.change_secret", + "authentik_crypto_secrets.delete_secret", + "authentik_crypto_secrets.rotate_secret", + "authentik_crypto_secrets.view_secret", + "authentik_crypto_secrets.view_secret_value", "authentik_endpoints.add_connector", "authentik_endpoints.add_device", "authentik_endpoints.add_deviceaccessgroup", @@ -7949,6 +8001,65 @@ } } }, + "model_authentik_crypto_secrets.secret": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "title": "Name" + }, + "type": { + "type": "string", + "enum": [ + "text", + "json", + "file" + ], + "title": "Type" + }, + "value": { + "type": "string", + "title": "Value" + }, + "length": { + "type": "integer", + "minimum": 1, + "maximum": 1024, + "title": "Length", + "description": "Length of the generated value when creating a text secret without a value. Defaults to the default token length." + } + }, + "required": [] + }, + "model_authentik_crypto_secrets.secret_permissions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "permission" + ], + "properties": { + "permission": { + "type": "string", + "enum": [ + "add_secret", + "change_secret", + "delete_secret", + "rotate_secret", + "view_secret", + "view_secret_value" + ] + }, + "user": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, "model_authentik_endpoints_connectors_agent.agentconnector": { "type": "object", "properties": { @@ -11003,6 +11114,7 @@ "authentik.api", "authentik.core", "authentik.crypto", + "authentik.crypto.secrets", "authentik.endpoints", "authentik.endpoints.connectors.agent", "authentik.enterprise", @@ -11098,6 +11210,7 @@ "authentik_core.objectattribute", "authentik_core.actor", "authentik_crypto.certificatekeypair", + "authentik_crypto_secrets.secret", "authentik_endpoints.deviceuserbinding", "authentik_endpoints.deviceaccessgroup", "authentik_endpoints.endpointstage", @@ -13560,6 +13673,12 @@ "authentik_crypto.view_certificatekeypair", "authentik_crypto.view_certificatekeypair_certificate", "authentik_crypto.view_certificatekeypair_key", + "authentik_crypto_secrets.add_secret", + "authentik_crypto_secrets.change_secret", + "authentik_crypto_secrets.delete_secret", + "authentik_crypto_secrets.rotate_secret", + "authentik_crypto_secrets.view_secret", + "authentik_crypto_secrets.view_secret_value", "authentik_endpoints.add_connector", "authentik_endpoints.add_device", "authentik_endpoints.add_deviceaccessgroup", diff --git a/packages/client-ts/src/apis/SecretsApi.ts b/packages/client-ts/src/apis/SecretsApi.ts new file mode 100644 index 000000000000..5f9958e9349a --- /dev/null +++ b/packages/client-ts/src/apis/SecretsApi.ts @@ -0,0 +1,779 @@ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.11.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 PaginatedSecretList, + PaginatedSecretListFromJSON, +} from "../models/PaginatedSecretList"; +import { + type PatchedSecretRequest, + PatchedSecretRequestToJSON, +} from "../models/PatchedSecretRequest"; +import { type RotatedSecret, RotatedSecretFromJSON } from "../models/RotatedSecret"; +import { type Secret, SecretFromJSON } from "../models/Secret"; +import { type SecretRequest, SecretRequestToJSON } from "../models/SecretRequest"; +import { type SecretTypeEnum } from "../models/SecretTypeEnum"; +import { type SecretValue, SecretValueFromJSON } from "../models/SecretValue"; +import { type UsedBy, UsedByFromJSON } from "../models/UsedBy"; +import * as runtime from "../runtime"; + +export interface SecretsSecretsCreateRequest { + secretRequest: SecretRequest; +} + +export interface SecretsSecretsDestroyRequest { + /** + * A UUID string identifying this Secret. + */ + secretUuid: string; +} + +export interface SecretsSecretsListRequest { + managed?: string; + name?: string; + /** + * Which field to use when ordering the results. + */ + ordering?: string; + /** + * A page number within the paginated result set. + */ + page?: number; + /** + * Number of results to return per page. + */ + pageSize?: number; + /** + * A search term. + */ + search?: string; + type?: SecretTypeEnum; + /** + * Multiple values may be separated by commas. + */ + typeIn?: Array; +} + +export interface SecretsSecretsPartialUpdateRequest { + /** + * A UUID string identifying this Secret. + */ + secretUuid: string; + patchedSecretRequest?: PatchedSecretRequest; +} + +export interface SecretsSecretsRetrieveRequest { + /** + * A UUID string identifying this Secret. + */ + secretUuid: string; +} + +export interface SecretsSecretsRotateCreateRequest { + /** + * A UUID string identifying this Secret. + */ + secretUuid: string; +} + +export interface SecretsSecretsUpdateRequest { + /** + * A UUID string identifying this Secret. + */ + secretUuid: string; + secretRequest: SecretRequest; +} + +export interface SecretsSecretsUsedByListRequest { + /** + * A UUID string identifying this Secret. + */ + secretUuid: string; +} + +export interface SecretsSecretsViewValueRetrieveRequest { + /** + * A UUID string identifying this Secret. + */ + secretUuid: string; +} + +export class SecretsApi extends runtime.BaseAPI { + /** + * Creates request options for secretsSecretsCreate without sending the request + */ + async secretsSecretsCreateRequestOpts( + requestParameters: SecretsSecretsCreateRequest, + ): Promise { + if (requestParameters["secretRequest"] == null) { + throw new runtime.RequiredError( + "secretRequest", + 'Required parameter "secretRequest" was null or undefined when calling secretsSecretsCreate().', + ); + } + + 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}`; + } + } + + const urlPath = `/secrets/secrets/`; + + return { + path: urlPath, + method: "POST", + headers: headerParameters, + query: queryParameters, + body: SecretRequestToJSON(requestParameters["secretRequest"]), + }; + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsCreateRaw( + requestParameters: SecretsSecretsCreateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.secretsSecretsCreateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SecretFromJSON(jsonValue)); + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsCreate( + requestParameters: SecretsSecretsCreateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.secretsSecretsCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for secretsSecretsDestroy without sending the request + */ + async secretsSecretsDestroyRequestOpts( + requestParameters: SecretsSecretsDestroyRequest, + ): Promise { + if (requestParameters["secretUuid"] == null) { + throw new runtime.RequiredError( + "secretUuid", + 'Required parameter "secretUuid" was null or undefined when calling secretsSecretsDestroy().', + ); + } + + 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 = `/secrets/secrets/{secret_uuid}/`; + urlPath = urlPath.replace( + "{secret_uuid}", + encodeURIComponent(String(requestParameters["secretUuid"])), + ); + + return { + path: urlPath, + method: "DELETE", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsDestroyRaw( + requestParameters: SecretsSecretsDestroyRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.secretsSecretsDestroyRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsDestroy( + requestParameters: SecretsSecretsDestroyRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + await this.secretsSecretsDestroyRaw(requestParameters, initOverrides); + } + + /** + * Creates request options for secretsSecretsList without sending the request + */ + async secretsSecretsListRequestOpts( + requestParameters: SecretsSecretsListRequest, + ): Promise { + const queryParameters: any = {}; + + if (requestParameters["managed"] != null) { + queryParameters["managed"] = requestParameters["managed"]; + } + + if (requestParameters["name"] != null) { + queryParameters["name"] = requestParameters["name"]; + } + + 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"]; + } + + if (requestParameters["type"] != null) { + queryParameters["type"] = requestParameters["type"]; + } + + if (requestParameters["typeIn"] != null) { + queryParameters["type__in"] = requestParameters["typeIn"]!.join( + runtime.COLLECTION_FORMATS["csv"], + ); + } + + 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}`; + } + } + + const urlPath = `/secrets/secrets/`; + + return { + path: urlPath, + method: "GET", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsListRaw( + requestParameters: SecretsSecretsListRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.secretsSecretsListRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => + PaginatedSecretListFromJSON(jsonValue), + ); + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsList( + requestParameters: SecretsSecretsListRequest = {}, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.secretsSecretsListRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for secretsSecretsPartialUpdate without sending the request + */ + async secretsSecretsPartialUpdateRequestOpts( + requestParameters: SecretsSecretsPartialUpdateRequest, + ): Promise { + if (requestParameters["secretUuid"] == null) { + throw new runtime.RequiredError( + "secretUuid", + 'Required parameter "secretUuid" was null or undefined when calling secretsSecretsPartialUpdate().', + ); + } + + 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 = `/secrets/secrets/{secret_uuid}/`; + urlPath = urlPath.replace( + "{secret_uuid}", + encodeURIComponent(String(requestParameters["secretUuid"])), + ); + + return { + path: urlPath, + method: "PATCH", + headers: headerParameters, + query: queryParameters, + body: PatchedSecretRequestToJSON(requestParameters["patchedSecretRequest"]), + }; + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsPartialUpdateRaw( + requestParameters: SecretsSecretsPartialUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.secretsSecretsPartialUpdateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SecretFromJSON(jsonValue)); + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsPartialUpdate( + requestParameters: SecretsSecretsPartialUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.secretsSecretsPartialUpdateRaw( + requestParameters, + initOverrides, + ); + return await response.value(); + } + + /** + * Creates request options for secretsSecretsRetrieve without sending the request + */ + async secretsSecretsRetrieveRequestOpts( + requestParameters: SecretsSecretsRetrieveRequest, + ): Promise { + if (requestParameters["secretUuid"] == null) { + throw new runtime.RequiredError( + "secretUuid", + 'Required parameter "secretUuid" was null or undefined when calling secretsSecretsRetrieve().', + ); + } + + 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 = `/secrets/secrets/{secret_uuid}/`; + urlPath = urlPath.replace( + "{secret_uuid}", + encodeURIComponent(String(requestParameters["secretUuid"])), + ); + + return { + path: urlPath, + method: "GET", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsRetrieveRaw( + requestParameters: SecretsSecretsRetrieveRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.secretsSecretsRetrieveRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SecretFromJSON(jsonValue)); + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsRetrieve( + requestParameters: SecretsSecretsRetrieveRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.secretsSecretsRetrieveRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for secretsSecretsRotateCreate without sending the request + */ + async secretsSecretsRotateCreateRequestOpts( + requestParameters: SecretsSecretsRotateCreateRequest, + ): Promise { + if (requestParameters["secretUuid"] == null) { + throw new runtime.RequiredError( + "secretUuid", + 'Required parameter "secretUuid" was null or undefined when calling secretsSecretsRotateCreate().', + ); + } + + 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 = `/secrets/secrets/{secret_uuid}/rotate/`; + urlPath = urlPath.replace( + "{secret_uuid}", + encodeURIComponent(String(requestParameters["secretUuid"])), + ); + + return { + path: urlPath, + method: "POST", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Replace a text secret with a generated value. + */ + async secretsSecretsRotateCreateRaw( + requestParameters: SecretsSecretsRotateCreateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.secretsSecretsRotateCreateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => + RotatedSecretFromJSON(jsonValue), + ); + } + + /** + * Replace a text secret with a generated value. + */ + async secretsSecretsRotateCreate( + requestParameters: SecretsSecretsRotateCreateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.secretsSecretsRotateCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for secretsSecretsUpdate without sending the request + */ + async secretsSecretsUpdateRequestOpts( + requestParameters: SecretsSecretsUpdateRequest, + ): Promise { + if (requestParameters["secretUuid"] == null) { + throw new runtime.RequiredError( + "secretUuid", + 'Required parameter "secretUuid" was null or undefined when calling secretsSecretsUpdate().', + ); + } + + if (requestParameters["secretRequest"] == null) { + throw new runtime.RequiredError( + "secretRequest", + 'Required parameter "secretRequest" was null or undefined when calling secretsSecretsUpdate().', + ); + } + + 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 = `/secrets/secrets/{secret_uuid}/`; + urlPath = urlPath.replace( + "{secret_uuid}", + encodeURIComponent(String(requestParameters["secretUuid"])), + ); + + return { + path: urlPath, + method: "PUT", + headers: headerParameters, + query: queryParameters, + body: SecretRequestToJSON(requestParameters["secretRequest"]), + }; + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsUpdateRaw( + requestParameters: SecretsSecretsUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.secretsSecretsUpdateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SecretFromJSON(jsonValue)); + } + + /** + * Create and manage named credentials referenced by authentik configuration objects. List and + * detail responses contain metadata only. Reading a value requires the view_value action and + * its separate permission. Replacing or manually rotating a value requires rotate_secret + * permission and records an audit event. Rotation changes the value stored in authentik; it + * does not update external systems. + */ + async secretsSecretsUpdate( + requestParameters: SecretsSecretsUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.secretsSecretsUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for secretsSecretsUsedByList without sending the request + */ + async secretsSecretsUsedByListRequestOpts( + requestParameters: SecretsSecretsUsedByListRequest, + ): Promise { + if (requestParameters["secretUuid"] == null) { + throw new runtime.RequiredError( + "secretUuid", + 'Required parameter "secretUuid" was null or undefined when calling secretsSecretsUsedByList().', + ); + } + + 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 = `/secrets/secrets/{secret_uuid}/used_by/`; + urlPath = urlPath.replace( + "{secret_uuid}", + encodeURIComponent(String(requestParameters["secretUuid"])), + ); + + return { + path: urlPath, + method: "GET", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Get a list of all objects that use this object + */ + async secretsSecretsUsedByListRaw( + requestParameters: SecretsSecretsUsedByListRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise>> { + const requestOptions = await this.secretsSecretsUsedByListRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(UsedByFromJSON)); + } + + /** + * Get a list of all objects that use this object + */ + async secretsSecretsUsedByList( + requestParameters: SecretsSecretsUsedByListRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const response = await this.secretsSecretsUsedByListRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for secretsSecretsViewValueRetrieve without sending the request + */ + async secretsSecretsViewValueRetrieveRequestOpts( + requestParameters: SecretsSecretsViewValueRetrieveRequest, + ): Promise { + if (requestParameters["secretUuid"] == null) { + throw new runtime.RequiredError( + "secretUuid", + 'Required parameter "secretUuid" was null or undefined when calling secretsSecretsViewValueRetrieve().', + ); + } + + 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 = `/secrets/secrets/{secret_uuid}/view_value/`; + urlPath = urlPath.replace( + "{secret_uuid}", + encodeURIComponent(String(requestParameters["secretUuid"])), + ); + + return { + path: urlPath, + method: "GET", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Return and audit a secret value. + */ + async secretsSecretsViewValueRetrieveRaw( + requestParameters: SecretsSecretsViewValueRetrieveRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = + await this.secretsSecretsViewValueRetrieveRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SecretValueFromJSON(jsonValue)); + } + + /** + * Return and audit a secret value. + */ + async secretsSecretsViewValueRetrieve( + requestParameters: SecretsSecretsViewValueRetrieveRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.secretsSecretsViewValueRetrieveRaw( + requestParameters, + initOverrides, + ); + return await response.value(); + } +} diff --git a/packages/client-ts/src/apis/index.ts b/packages/client-ts/src/apis/index.ts index 3d758b9b0c49..e3b97b8b4fe6 100644 --- a/packages/client-ts/src/apis/index.ts +++ b/packages/client-ts/src/apis/index.ts @@ -21,6 +21,7 @@ export * from "./ReportsApi"; export * from "./RequestsApi"; export * from "./RootApi"; export * from "./SchemaApi"; +export * from "./SecretsApi"; export * from "./SourcesApi"; export * from "./SsfApi"; export * from "./StagesApi"; diff --git a/packages/client-ts/src/models/AppEnum.ts b/packages/client-ts/src/models/AppEnum.ts index 932b9f01e7bd..b4b9268898a7 100644 --- a/packages/client-ts/src/models/AppEnum.ts +++ b/packages/client-ts/src/models/AppEnum.ts @@ -20,6 +20,7 @@ export const AppEnum = { AuthentikApi: "authentik.api", AuthentikCore: "authentik.core", AuthentikCrypto: "authentik.crypto", + AuthentikCryptoSecrets: "authentik.crypto.secrets", AuthentikEndpoints: "authentik.endpoints", AuthentikEndpointsConnectorsAgent: "authentik.endpoints.connectors.agent", AuthentikEnterprise: "authentik.enterprise", diff --git a/packages/client-ts/src/models/ModelEnum.ts b/packages/client-ts/src/models/ModelEnum.ts index d73d8ad2c579..48d5592606f5 100644 --- a/packages/client-ts/src/models/ModelEnum.ts +++ b/packages/client-ts/src/models/ModelEnum.ts @@ -22,6 +22,7 @@ export const ModelEnum = { AuthentikCoreObjectattribute: "authentik_core.objectattribute", AuthentikCoreActor: "authentik_core.actor", AuthentikCryptoCertificatekeypair: "authentik_crypto.certificatekeypair", + AuthentikCryptoSecretsSecret: "authentik_crypto_secrets.secret", AuthentikEndpointsDeviceuserbinding: "authentik_endpoints.deviceuserbinding", AuthentikEndpointsDeviceaccessgroup: "authentik_endpoints.deviceaccessgroup", AuthentikEndpointsEndpointstage: "authentik_endpoints.endpointstage", diff --git a/packages/client-ts/src/models/PaginatedSecretList.ts b/packages/client-ts/src/models/PaginatedSecretList.ts new file mode 100644 index 000000000000..6db32e028c25 --- /dev/null +++ b/packages/client-ts/src/models/PaginatedSecretList.ts @@ -0,0 +1,73 @@ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.11.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 { Pagination } from "./Pagination"; +import { PaginationFromJSON, PaginationToJSON } from "./Pagination"; +import type { Secret } from "./Secret"; +import { SecretFromJSON, SecretToJSON } from "./Secret"; + +/** + * @export + * @interface PaginatedSecretList + */ +export interface PaginatedSecretList { + pagination: Pagination; + results: Array; + autocomplete: { [key: string]: any }; +} + +/** + * Check if a given object implements the PaginatedSecretList interface. + */ +export function instanceOfPaginatedSecretList(value: object): value is PaginatedSecretList { + 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 PaginatedSecretListFromJSON(json: any): PaginatedSecretList { + return PaginatedSecretListFromJSONTyped(json, false); +} + +export function PaginatedSecretListFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): PaginatedSecretList { + if (json == null) { + return json; + } + return { + pagination: PaginationFromJSON(json["pagination"]), + results: (json["results"] as Array).map(SecretFromJSON), + autocomplete: json["autocomplete"], + }; +} + +export function PaginatedSecretListToJSON(json: any): PaginatedSecretList { + return PaginatedSecretListToJSONTyped(json, false); +} + +export function PaginatedSecretListToJSONTyped( + value?: PaginatedSecretList | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return { + pagination: PaginationToJSON(value["pagination"]), + results: (value["results"] as Array).map(SecretToJSON), + autocomplete: value["autocomplete"], + }; +} diff --git a/packages/client-ts/src/models/PatchedSecretRequest.ts b/packages/client-ts/src/models/PatchedSecretRequest.ts new file mode 100644 index 000000000000..1e4631767ae2 --- /dev/null +++ b/packages/client-ts/src/models/PatchedSecretRequest.ts @@ -0,0 +1,77 @@ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.11.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 { SecretTypeEnum } from "./SecretTypeEnum"; +import { SecretTypeEnumFromJSON, SecretTypeEnumToJSON } from "./SecretTypeEnum"; + +/** + * Create and configure a secret without exposing its value. + * + * @export + * @interface PatchedSecretRequest + */ +export interface PatchedSecretRequest { + name?: string; + type?: SecretTypeEnum; + value?: string; + /** + * Length of the generated value when creating a text secret without a value. Defaults to the + * default token length. + */ + length?: number; +} + +/** + * Check if a given object implements the PatchedSecretRequest interface. + */ +export function instanceOfPatchedSecretRequest(value: object): value is PatchedSecretRequest { + return true; +} + +export function PatchedSecretRequestFromJSON(json: any): PatchedSecretRequest { + return PatchedSecretRequestFromJSONTyped(json, false); +} + +export function PatchedSecretRequestFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): PatchedSecretRequest { + if (json == null) { + return json; + } + return { + name: json["name"] == null ? undefined : json["name"], + type: json["type"] == null ? undefined : SecretTypeEnumFromJSON(json["type"]), + value: json["value"] == null ? undefined : json["value"], + length: json["length"] == null ? undefined : json["length"], + }; +} + +export function PatchedSecretRequestToJSON(json: any): PatchedSecretRequest { + return PatchedSecretRequestToJSONTyped(json, false); +} + +export function PatchedSecretRequestToJSONTyped( + value?: PatchedSecretRequest | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return { + name: value["name"], + type: SecretTypeEnumToJSON(value["type"]), + value: value["value"], + length: value["length"], + }; +} diff --git a/packages/client-ts/src/models/RotatedSecret.ts b/packages/client-ts/src/models/RotatedSecret.ts new file mode 100644 index 000000000000..893655653fed --- /dev/null +++ b/packages/client-ts/src/models/RotatedSecret.ts @@ -0,0 +1,57 @@ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.11.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. + */ + +/** + * A rotated value, hidden when the caller cannot view it. + * + * @export + * @interface RotatedSecret + */ +export interface RotatedSecret { + readonly value: string | null; +} + +/** + * Check if a given object implements the RotatedSecret interface. + */ +export function instanceOfRotatedSecret(value: object): value is RotatedSecret { + if (!("value" in value) || value["value"] === undefined) return false; + return true; +} + +export function RotatedSecretFromJSON(json: any): RotatedSecret { + return RotatedSecretFromJSONTyped(json, false); +} + +export function RotatedSecretFromJSONTyped(json: any, ignoreDiscriminator: boolean): RotatedSecret { + if (json == null) { + return json; + } + return { + value: json["value"], + }; +} + +export function RotatedSecretToJSON(json: any): RotatedSecret { + return RotatedSecretToJSONTyped(json, false); +} + +export function RotatedSecretToJSONTyped( + value?: Omit | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return {}; +} diff --git a/packages/client-ts/src/models/Secret.ts b/packages/client-ts/src/models/Secret.ts new file mode 100644 index 000000000000..d786321373b9 --- /dev/null +++ b/packages/client-ts/src/models/Secret.ts @@ -0,0 +1,92 @@ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.11.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 { parseDateTime } from "../runtime"; +import type { SecretTypeEnum } from "./SecretTypeEnum"; +import { SecretTypeEnumFromJSON, SecretTypeEnumToJSON } from "./SecretTypeEnum"; + +/** + * Create and configure a secret without exposing its value. + * + * @export + * @interface Secret + */ +export interface Secret { + readonly pk: string; + name: string; + type?: SecretTypeEnum; + /** + * 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. + */ + readonly managed: string | null; + readonly created: Date; + readonly lastUpdated: Date; +} + +/** + * Check if a given object implements the Secret interface. + */ +export function instanceOfSecret(value: object): value is Secret { + if (!("pk" in value) || value["pk"] === undefined) return false; + if (!("name" in value) || value["name"] === undefined) return false; + if (!("managed" in value) || value["managed"] === undefined) return false; + if (!("created" in value) || value["created"] === undefined) return false; + if ( + (!("lastUpdated" in (value as Record)) && + !("last_updated" in (value as Record))) || + ((value as Record)["lastUpdated"] === undefined && + (value as Record)["last_updated"] === undefined) + ) + return false; + return true; +} + +export function SecretFromJSON(json: any): Secret { + return SecretFromJSONTyped(json, false); +} + +export function SecretFromJSONTyped(json: any, ignoreDiscriminator: boolean): Secret { + if (json == null) { + return json; + } + return { + pk: json["pk"], + name: json["name"], + type: json["type"] == null ? undefined : SecretTypeEnumFromJSON(json["type"]), + managed: json["managed"], + created: json["created"] == null ? json["created"] : parseDateTime(json["created"]), + lastUpdated: + json["last_updated"] == null + ? json["last_updated"] + : parseDateTime(json["last_updated"]), + }; +} + +export function SecretToJSON(json: any): Secret { + return SecretToJSONTyped(json, false); +} + +export function SecretToJSONTyped( + value?: Omit | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return { + name: value["name"], + type: SecretTypeEnumToJSON(value["type"]), + }; +} diff --git a/packages/client-ts/src/models/SecretRequest.ts b/packages/client-ts/src/models/SecretRequest.ts new file mode 100644 index 000000000000..c4991beab3e7 --- /dev/null +++ b/packages/client-ts/src/models/SecretRequest.ts @@ -0,0 +1,75 @@ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.11.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 { SecretTypeEnum } from "./SecretTypeEnum"; +import { SecretTypeEnumFromJSON, SecretTypeEnumToJSON } from "./SecretTypeEnum"; + +/** + * Create and configure a secret without exposing its value. + * + * @export + * @interface SecretRequest + */ +export interface SecretRequest { + name: string; + type?: SecretTypeEnum; + value?: string; + /** + * Length of the generated value when creating a text secret without a value. Defaults to the + * default token length. + */ + length?: number; +} + +/** + * Check if a given object implements the SecretRequest interface. + */ +export function instanceOfSecretRequest(value: object): value is SecretRequest { + if (!("name" in value) || value["name"] === undefined) return false; + return true; +} + +export function SecretRequestFromJSON(json: any): SecretRequest { + return SecretRequestFromJSONTyped(json, false); +} + +export function SecretRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): SecretRequest { + if (json == null) { + return json; + } + return { + name: json["name"], + type: json["type"] == null ? undefined : SecretTypeEnumFromJSON(json["type"]), + value: json["value"] == null ? undefined : json["value"], + length: json["length"] == null ? undefined : json["length"], + }; +} + +export function SecretRequestToJSON(json: any): SecretRequest { + return SecretRequestToJSONTyped(json, false); +} + +export function SecretRequestToJSONTyped( + value?: SecretRequest | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return { + name: value["name"], + type: SecretTypeEnumToJSON(value["type"]), + value: value["value"], + length: value["length"], + }; +} diff --git a/packages/client-ts/src/models/SecretTypeEnum.ts b/packages/client-ts/src/models/SecretTypeEnum.ts new file mode 100644 index 000000000000..66b258bd4b89 --- /dev/null +++ b/packages/client-ts/src/models/SecretTypeEnum.ts @@ -0,0 +1,55 @@ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.11.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 SecretTypeEnum = { + Text: "text", + Json: "json", + File: "file", + UnknownDefaultOpenApi: "11184809", +} as const; +export type SecretTypeEnum = (typeof SecretTypeEnum)[keyof typeof SecretTypeEnum]; + +export function instanceOfSecretTypeEnum(value: any): boolean { + for (const key in SecretTypeEnum) { + if (Object.prototype.hasOwnProperty.call(SecretTypeEnum, key)) { + if (SecretTypeEnum[key as keyof typeof SecretTypeEnum] === value) { + return true; + } + } + } + return false; +} + +export function SecretTypeEnumFromJSON(json: any): SecretTypeEnum { + return SecretTypeEnumFromJSONTyped(json, false); +} + +export function SecretTypeEnumFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): SecretTypeEnum { + return json as SecretTypeEnum; +} + +export function SecretTypeEnumToJSON(value?: SecretTypeEnum | null): any { + return value as any; +} + +export function SecretTypeEnumToJSONTyped( + value: any, + ignoreDiscriminator: boolean, +): SecretTypeEnum { + return value as SecretTypeEnum; +} diff --git a/packages/client-ts/src/models/SecretValue.ts b/packages/client-ts/src/models/SecretValue.ts new file mode 100644 index 000000000000..9bc1b4479b46 --- /dev/null +++ b/packages/client-ts/src/models/SecretValue.ts @@ -0,0 +1,57 @@ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.11.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. + */ + +/** + * A secret value. + * + * @export + * @interface SecretValue + */ +export interface SecretValue { + readonly value: string; +} + +/** + * Check if a given object implements the SecretValue interface. + */ +export function instanceOfSecretValue(value: object): value is SecretValue { + if (!("value" in value) || value["value"] === undefined) return false; + return true; +} + +export function SecretValueFromJSON(json: any): SecretValue { + return SecretValueFromJSONTyped(json, false); +} + +export function SecretValueFromJSONTyped(json: any, ignoreDiscriminator: boolean): SecretValue { + if (json == null) { + return json; + } + return { + value: json["value"], + }; +} + +export function SecretValueToJSON(json: any): SecretValue { + return SecretValueToJSONTyped(json, false); +} + +export function SecretValueToJSONTyped( + value?: Omit | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return {}; +} diff --git a/packages/client-ts/src/models/UsedByActionEnum.ts b/packages/client-ts/src/models/UsedByActionEnum.ts index 71e0192e2728..eef7a7d86efb 100644 --- a/packages/client-ts/src/models/UsedByActionEnum.ts +++ b/packages/client-ts/src/models/UsedByActionEnum.ts @@ -18,6 +18,7 @@ export const UsedByActionEnum = { CascadeMany: "cascade_many", SetNull: "set_null", SetDefault: "set_default", + Protect: "protect", LeftDangling: "left_dangling", UnknownDefaultOpenApi: "11184809", } as const; diff --git a/packages/client-ts/src/models/index.ts b/packages/client-ts/src/models/index.ts index 5902e587a6bf..44e58d81da63 100644 --- a/packages/client-ts/src/models/index.ts +++ b/packages/client-ts/src/models/index.ts @@ -504,6 +504,7 @@ export * from "./PaginatedSSFProviderList"; export * from "./PaginatedSSFStreamList"; export * from "./PaginatedScheduleList"; export * from "./PaginatedScopeMappingList"; +export * from "./PaginatedSecretList"; export * from "./PaginatedServiceConnectionList"; export * from "./PaginatedSourceList"; export * from "./PaginatedSourceStageList"; @@ -655,6 +656,7 @@ export * from "./PatchedSMSDeviceRequest"; export * from "./PatchedSSFProviderRequest"; export * from "./PatchedScheduleRequest"; export * from "./PatchedScopeMappingRequest"; +export * from "./PatchedSecretRequest"; export * from "./PatchedSettingsRequest"; export * from "./PatchedSettingsRequestFlags"; export * from "./PatchedSourceStageRequest"; @@ -764,6 +766,7 @@ export * from "./RoleAssignedObjectPermission"; export * from "./RoleModelPermission"; export * from "./RoleObjectPermission"; export * from "./RoleRequest"; +export * from "./RotatedSecret"; export * from "./SAMLBindingsEnum"; export * from "./SAMLLogoutMethods"; export * from "./SAMLMetadata"; @@ -803,6 +806,10 @@ export * from "./Schedule"; export * from "./ScheduleRequest"; export * from "./ScopeMapping"; export * from "./ScopeMappingRequest"; +export * from "./Secret"; +export * from "./SecretRequest"; +export * from "./SecretTypeEnum"; +export * from "./SecretValue"; export * from "./SelectableStage"; export * from "./ServerLogRequest"; export * from "./ServiceBindMethodEnum"; diff --git a/schema.yml b/schema.yml index 6962a0445d0f..e0c5058f1492 100644 --- a/schema.yml +++ b/schema.yml @@ -23276,6 +23276,303 @@ paths: $ref: '#/components/responses/ValidationErrorResponse' '403': $ref: '#/components/responses/GenericErrorResponse' + /secrets/secrets/: + get: + operationId: secrets_secrets_list + description: |- + Create and manage named credentials referenced by authentik configuration objects. + + List and detail responses contain metadata only. Reading a value requires the + view_value action and its separate permission. Replacing or manually rotating + a value requires rotate_secret permission and records an audit event. + Rotation changes the value stored in authentik; it does not update external systems. + parameters: + - in: query + name: managed + schema: + type: string + - $ref: '#/components/parameters/QueryName' + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QueryPaginationPage' + - $ref: '#/components/parameters/QueryPaginationPageSize' + - $ref: '#/components/parameters/QuerySearch' + - in: query + name: type + schema: + $ref: '#/components/schemas/SecretTypeEnum' + - in: query + name: type__in + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + tags: + - secrets + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedSecretList' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + post: + operationId: secrets_secrets_create + description: |- + Create and manage named credentials referenced by authentik configuration objects. + + List and detail responses contain metadata only. Reading a value requires the + view_value action and its separate permission. Replacing or manually rotating + a value requires rotate_secret permission and records an audit event. + Rotation changes the value stored in authentik; it does not update external systems. + tags: + - secrets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SecretRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + /secrets/secrets/{secret_uuid}/: + get: + operationId: secrets_secrets_retrieve + description: |- + Create and manage named credentials referenced by authentik configuration objects. + + List and detail responses contain metadata only. Reading a value requires the + view_value action and its separate permission. Replacing or manually rotating + a value requires rotate_secret permission and records an audit event. + Rotation changes the value stored in authentik; it does not update external systems. + parameters: + - in: path + name: secret_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Secret. + required: true + tags: + - secrets + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + put: + operationId: secrets_secrets_update + description: |- + Create and manage named credentials referenced by authentik configuration objects. + + List and detail responses contain metadata only. Reading a value requires the + view_value action and its separate permission. Replacing or manually rotating + a value requires rotate_secret permission and records an audit event. + Rotation changes the value stored in authentik; it does not update external systems. + parameters: + - in: path + name: secret_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Secret. + required: true + tags: + - secrets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SecretRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + patch: + operationId: secrets_secrets_partial_update + description: |- + Create and manage named credentials referenced by authentik configuration objects. + + List and detail responses contain metadata only. Reading a value requires the + view_value action and its separate permission. Replacing or manually rotating + a value requires rotate_secret permission and records an audit event. + Rotation changes the value stored in authentik; it does not update external systems. + parameters: + - in: path + name: secret_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Secret. + required: true + tags: + - secrets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSecretRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Secret' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + delete: + operationId: secrets_secrets_destroy + description: |- + Create and manage named credentials referenced by authentik configuration objects. + + List and detail responses contain metadata only. Reading a value requires the + view_value action and its separate permission. Replacing or manually rotating + a value requires rotate_secret permission and records an audit event. + Rotation changes the value stored in authentik; it does not update external systems. + parameters: + - in: path + name: secret_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Secret. + required: true + tags: + - secrets + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + /secrets/secrets/{secret_uuid}/rotate/: + post: + operationId: secrets_secrets_rotate_create + description: Replace a text secret with a generated value. + parameters: + - in: path + name: secret_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Secret. + required: true + tags: + - secrets + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/RotatedSecret' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + /secrets/secrets/{secret_uuid}/used_by/: + get: + operationId: secrets_secrets_used_by_list + description: Get a list of all objects that use this object + parameters: + - in: path + name: secret_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Secret. + required: true + tags: + - secrets + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UsedBy' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + /secrets/secrets/{secret_uuid}/view_value/: + get: + operationId: secrets_secrets_view_value_retrieve + description: Return and audit a secret value. + parameters: + - in: path + name: secret_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Secret. + required: true + tags: + - secrets + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SecretValue' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' /sources/all/: get: operationId: sources_all_list @@ -35748,6 +36045,7 @@ components: - authentik.api - authentik.core - authentik.crypto + - authentik.crypto.secrets - authentik.endpoints - authentik.endpoints.connectors.agent - authentik.enterprise @@ -45111,6 +45409,7 @@ components: - authentik_core.objectattribute - authentik_core.actor - authentik_crypto.certificatekeypair + - authentik_crypto_secrets.secret - authentik_endpoints.deviceuserbinding - authentik_endpoints.deviceaccessgroup - authentik_endpoints.endpointstage @@ -48885,6 +49184,21 @@ components: - autocomplete - pagination - results + PaginatedSecretList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/Secret' + autocomplete: + $ref: '#/components/schemas/Autocomplete' + required: + - autocomplete + - pagination + - results PaginatedServiceConnectionList: type: object properties: @@ -53384,6 +53698,25 @@ components: type: string description: Description shown to the user when consenting. If left empty, the user won't be informed. + PatchedSecretRequest: + type: object + description: Create and configure a secret without exposing its value. + properties: + name: + type: string + minLength: 1 + type: + $ref: '#/components/schemas/SecretTypeEnum' + value: + type: string + writeOnly: true + length: + type: integer + maximum: 1024 + minimum: 1 + writeOnly: true + description: Length of the generated value when creating a text secret without + a value. Defaults to the default token length. PatchedSettingsRequest: type: object description: Settings Serializer @@ -56633,6 +56966,16 @@ components: minLength: 1 required: - name + RotatedSecret: + type: object + description: A rotated value, hidden when the caller cannot view it. + properties: + value: + type: string + readOnly: true + nullable: true + required: + - value SAMLBindingsEnum: enum: - redirect @@ -58474,6 +58817,78 @@ components: - expression - name - scope_name + Secret: + type: object + description: Create and configure a secret without exposing its value. + properties: + pk: + type: string + format: uuid + readOnly: true + title: Secret uuid + name: + type: string + type: + $ref: '#/components/schemas/SecretTypeEnum' + managed: + type: string + readOnly: true + 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. + created: + type: string + format: date-time + readOnly: true + last_updated: + type: string + format: date-time + readOnly: true + required: + - created + - last_updated + - managed + - name + - pk + SecretRequest: + type: object + description: Create and configure a secret without exposing its value. + properties: + name: + type: string + minLength: 1 + type: + $ref: '#/components/schemas/SecretTypeEnum' + value: + type: string + writeOnly: true + length: + type: integer + maximum: 1024 + minimum: 1 + writeOnly: true + description: Length of the generated value when creating a text secret without + a value. Defaults to the default token length. + required: + - name + SecretTypeEnum: + enum: + - text + - json + - file + type: string + SecretValue: + type: object + description: A secret value. + properties: + value: + type: string + readOnly: true + required: + - value SelectableStage: type: object description: Serializer for stages which can be selected by users @@ -60160,6 +60575,7 @@ components: - cascade_many - set_null - set_default + - protect - left_dangling type: string User: diff --git a/web/src/elements/entities/used-by.ts b/web/src/elements/entities/used-by.ts index 16e9d1165a99..a4aaf969bf26 100644 --- a/web/src/elements/entities/used-by.ts +++ b/web/src/elements/entities/used-by.ts @@ -35,6 +35,14 @@ export function formatUsedByConsequence(usedBy: UsedBy, verboseName?: string): s id: "used-by.consequence.set-null", }), ) + .with(UsedByActionEnum.Protect, () => { + const relationName = usedBy.modelName || msg("Related object"); + + return msg(str`${relationName} still uses it, so it can't be deleted`, { + id: "used-by.consequence.protect", + desc: "Consequence of deletion, when a related object prevents the deletion.", + }); + }) .with(UsedByActionEnum.LeftDangling, () => msg(str`${verboseName} will be left dangling (may cause errors)`, { id: "used-by.consequence.left-dangling",