mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 15:42:48 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e7fa6bc0 | ||
|
|
4b4968c66b | ||
|
|
4e5b938ebe |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -64,7 +64,7 @@ runs:
|
||||
rustflags: ""
|
||||
- name: Setup rust dependencies
|
||||
if: ${{ contains(inputs.dependencies, 'rust') }}
|
||||
uses: taiki-e/install-action@cf525cb33f51aca27cd6fa02034117ab963ff9f1 # v2
|
||||
uses: taiki-e/install-action@787505cde8a44ea468a00478fe52baf23b15bccd # v2
|
||||
with:
|
||||
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
|
||||
- name: Setup node (web)
|
||||
|
||||
@@ -14,7 +14,6 @@ pyproject.toml @goauthentik/backend
|
||||
uv.lock @goauthentik/backend
|
||||
Cargo.toml @goauthentik/backend
|
||||
Cargo.lock @goauthentik/backend
|
||||
build.rs @goauthentik/backend
|
||||
go.mod @goauthentik/backend
|
||||
go.sum @goauthentik/backend
|
||||
.cargo/ @goauthentik/backend
|
||||
|
||||
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -198,7 +198,6 @@ dependencies = [
|
||||
"metrics-exporter-prometheus",
|
||||
"nix 0.31.2",
|
||||
"pyo3",
|
||||
"pyo3-build-config",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3000,9 +2999,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.40"
|
||||
version = "0.23.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
|
||||
@@ -49,7 +49,6 @@ nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
|
||||
notify = "= 8.2.0"
|
||||
pin-project-lite = "= 0.2.17"
|
||||
pyo3 = "= 0.28.3"
|
||||
pyo3-build-config = "= 0.28.3"
|
||||
regex = "= 1.12.3"
|
||||
reqwest = { version = "= 0.13.2", features = [
|
||||
"form",
|
||||
@@ -66,7 +65,7 @@ reqwest-middleware = { version = "= 0.5.1", features = [
|
||||
"query",
|
||||
"rustls",
|
||||
] }
|
||||
rustls = { version = "= 0.23.40", features = ["fips"] }
|
||||
rustls = { version = "= 0.23.39", features = ["fips"] }
|
||||
sentry = { version = "= 0.47.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
@@ -261,9 +260,6 @@ default = ["core", "proxy"]
|
||||
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
|
||||
proxy = ["ak-common/proxy"]
|
||||
|
||||
[build-dependencies]
|
||||
pyo3-build-config.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ak-axum.workspace = true
|
||||
ak-common.workspace = true
|
||||
|
||||
@@ -66,4 +66,5 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
|
||||
"footer_links": tenant.footer_links,
|
||||
"html_meta": {**get_http_meta()},
|
||||
"version": authentik_full_version(),
|
||||
"csp_nonce": request.request_id,
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
|
||||
SAML_STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
|
||||
|
||||
DEFAULT_ISSUER = "authentik"
|
||||
|
||||
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
||||
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
|
||||
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.2
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Iterator
|
||||
from copy import copy
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Case, QuerySet
|
||||
from django.db.models import Case, Q, QuerySet
|
||||
from django.db.models.expressions import When
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -120,7 +120,6 @@ class ApplicationSerializer(ModelSerializer):
|
||||
"meta_publisher",
|
||||
"policy_engine_mode",
|
||||
"group",
|
||||
"meta_hide",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"backchannel_providers": {"required": False},
|
||||
@@ -284,12 +283,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
) == "true"
|
||||
|
||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
queryset = queryset.exclude(meta_hide=True)
|
||||
if only_with_launch_url:
|
||||
# Pre-filter at DB level to skip expensive per-app policy evaluation
|
||||
# for apps that can never appear in the launcher (no meta_launch_url
|
||||
# and no provider, so no possible launch URL).
|
||||
queryset = queryset.exclude(meta_launch_url="", provider__isnull=True)
|
||||
# for apps that can never appear in the launcher:
|
||||
# - No meta_launch_url AND no provider: no possible launch URL
|
||||
# - meta_launch_url="blank://blank": documented convention to hide from launcher
|
||||
queryset = queryset.exclude(
|
||||
Q(meta_launch_url="", provider__isnull=True) | Q(meta_launch_url="blank://blank")
|
||||
)
|
||||
paginator: Pagination = self.paginator
|
||||
paginated_apps = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField
|
||||
from rest_framework.relations import PrimaryKeyRelatedField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ListSerializer, ValidationError
|
||||
@@ -37,77 +37,6 @@ from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.rbac.api.roles import RoleSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
|
||||
class BulkManyRelatedField(ManyRelatedField):
|
||||
"""ManyRelatedField that validates all PKs in a single query instead of one per PK."""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, str) or not hasattr(data, "__iter__"):
|
||||
self.fail("not_a_list", input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail("empty")
|
||||
|
||||
child = self.child_relation
|
||||
pk_field = child.pk_field
|
||||
# Coerce PKs through pk_field if defined
|
||||
pk_map = {}
|
||||
for item in data:
|
||||
if isinstance(item, bool):
|
||||
self.fail("incorrect_type", data_type=type(item).__name__)
|
||||
pk = pk_field.to_internal_value(item) if pk_field else item
|
||||
pk_map[pk] = item # map coerced PK -> original value for error reporting
|
||||
|
||||
queryset = child.get_queryset()
|
||||
# Use count to validate all PKs exist in a single query
|
||||
found_count = queryset.filter(pk__in=pk_map.keys()).count()
|
||||
if found_count < len(pk_map):
|
||||
# Some PKs not found — fall back to per-PK checks for error reporting.
|
||||
# This only runs when there's an actual validation error (rare path).
|
||||
for pk, original in pk_map.items():
|
||||
if not queryset.filter(pk=pk).exists():
|
||||
child.fail("does_not_exist", pk_value=original)
|
||||
|
||||
# Return raw PKs — Django's M2M set() accepts both objects and PKs,
|
||||
# using get_prep_value() for type coercion. This avoids loading all
|
||||
# objects into memory and avoids triggering post_init signals.
|
||||
return list(pk_map.keys())
|
||||
|
||||
def to_representation(self, iterable):
|
||||
# For non-prefetched querysets, get PKs directly without loading model instances.
|
||||
# When prefetched, _result_cache is a list (possibly empty); when not, it's None.
|
||||
if hasattr(iterable, "values_list") and getattr(iterable, "_result_cache", None) is None:
|
||||
return list(iterable.values_list("pk", flat=True))
|
||||
return super().to_representation(iterable)
|
||||
|
||||
|
||||
class BulkPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
||||
"""PrimaryKeyRelatedField that uses bulk validation when many=True."""
|
||||
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
allow_empty = kwargs.pop("allow_empty", None)
|
||||
max_length = kwargs.pop("max_length", None)
|
||||
min_length = kwargs.pop("min_length", None)
|
||||
child_relation = cls(*args, **kwargs)
|
||||
list_kwargs = {
|
||||
"child_relation": child_relation,
|
||||
}
|
||||
if allow_empty is not None:
|
||||
list_kwargs["allow_empty"] = allow_empty
|
||||
if max_length is not None:
|
||||
list_kwargs["max_length"] = max_length
|
||||
if min_length is not None:
|
||||
list_kwargs["min_length"] = min_length
|
||||
list_kwargs.update(
|
||||
{
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key in ("required", "default", "source")
|
||||
}
|
||||
)
|
||||
return BulkManyRelatedField(**list_kwargs)
|
||||
|
||||
|
||||
PARTIAL_USER_SERIALIZER_MODEL_FIELDS = [
|
||||
"pk",
|
||||
"username",
|
||||
@@ -150,7 +79,6 @@ class GroupSerializer(ModelSerializer):
|
||||
"""Group Serializer"""
|
||||
|
||||
attributes = JSONDictField(required=False)
|
||||
users = BulkPrimaryKeyRelatedField(queryset=User.objects.all(), many=True, default=list)
|
||||
parents = PrimaryKeyRelatedField(queryset=Group.objects.all(), many=True, required=False)
|
||||
parents_obj = SerializerMethodField(allow_null=True)
|
||||
children_obj = SerializerMethodField(allow_null=True)
|
||||
@@ -265,6 +193,9 @@ class GroupSerializer(ModelSerializer):
|
||||
"children_obj",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"users": {
|
||||
"default": list,
|
||||
},
|
||||
"children": {
|
||||
"required": False,
|
||||
"default": list,
|
||||
@@ -294,7 +225,6 @@ class GroupFilter(FilterSet):
|
||||
members_by_pk = ModelMultipleChoiceFilter(
|
||||
field_name="users",
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
)
|
||||
|
||||
def filter_attributes(self, queryset, name, value):
|
||||
@@ -346,8 +276,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
# Always prefetch parents and children since their PKs are always serialized
|
||||
base_qs = Group.objects.all().prefetch_related("roles", "parents", "children")
|
||||
base_qs = Group.objects.all().prefetch_related("roles")
|
||||
|
||||
if self.serializer_class(context={"request": self.request})._should_include_users:
|
||||
# Only fetch fields needed by PartialUserSerializer to reduce DB load and instantiation
|
||||
@@ -358,9 +287,16 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset=User.objects.all().only(*PARTIAL_USER_SERIALIZER_MODEL_FIELDS),
|
||||
)
|
||||
)
|
||||
# When include_users=false, skip users prefetch entirely.
|
||||
# BulkManyRelatedField.to_representation will use values_list to get PKs
|
||||
# directly without loading User instances into memory.
|
||||
else:
|
||||
base_qs = base_qs.prefetch_related(
|
||||
Prefetch("users", queryset=User.objects.all().only("id"))
|
||||
)
|
||||
|
||||
if self.serializer_class(context={"request": self.request})._should_include_children:
|
||||
base_qs = base_qs.prefetch_related("children")
|
||||
|
||||
if self.serializer_class(context={"request": self.request})._should_include_parents:
|
||||
base_qs = base_qs.prefetch_related("parents")
|
||||
|
||||
return base_qs
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any
|
||||
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import AnonymousUser, Permission
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
from django.urls import reverse_lazy
|
||||
@@ -14,7 +13,6 @@ from django.utils.http import urlencode
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django_filters.filters import (
|
||||
BooleanFilter,
|
||||
CharFilter,
|
||||
@@ -107,10 +105,6 @@ from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
INVALID_PASSWORD_HASH_MESSAGE = gettext_lazy(
|
||||
"Invalid password hash format. Must be a valid Django password hash."
|
||||
)
|
||||
|
||||
|
||||
class ParamUserSerializer(PassiveSerializer):
|
||||
"""Partial serializer for query parameters to select a user"""
|
||||
@@ -137,7 +131,7 @@ class PartialGroupSerializer(ModelSerializer):
|
||||
class UserSerializer(ModelSerializer):
|
||||
"""User Serializer"""
|
||||
|
||||
is_superuser = SerializerMethodField()
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = SerializerMethodField()
|
||||
attributes = JSONDictField(required=False)
|
||||
groups = PrimaryKeyRelatedField(
|
||||
@@ -174,14 +168,6 @@ class UserSerializer(ModelSerializer):
|
||||
return True
|
||||
return str(request.query_params.get("include_roles", "true")).lower() == "true"
|
||||
|
||||
@extend_schema_field(BooleanField)
|
||||
def get_is_superuser(self, instance: User) -> bool:
|
||||
"""Use annotation if available to avoid N+1 query"""
|
||||
ann = getattr(instance, "_annotated_is_superuser", None)
|
||||
if ann is not None:
|
||||
return ann
|
||||
return instance.is_superuser
|
||||
|
||||
@extend_schema_field(PartialGroupSerializer(many=True))
|
||||
def get_groups_obj(self, instance: User) -> list[PartialGroupSerializer] | None:
|
||||
if not self._should_include_groups:
|
||||
@@ -195,79 +181,47 @@ class UserSerializer(ModelSerializer):
|
||||
return RoleSerializer(instance.roles, many=True).data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Setting password and permissions directly is allowed only in blueprints."""
|
||||
super().__init__(*args, **kwargs)
|
||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||
self.fields["password"] = CharField(required=False, allow_null=True)
|
||||
self.fields["password_hash"] = CharField(required=False, allow_null=True)
|
||||
self.fields["permissions"] = ListField(
|
||||
required=False,
|
||||
child=ChoiceField(choices=get_permission_choices()),
|
||||
)
|
||||
|
||||
def create(self, validated_data: dict) -> User:
|
||||
"""Create a user, with blueprint-only password and permission writes."""
|
||||
is_blueprint = SERIALIZER_CONTEXT_BLUEPRINT in self.context
|
||||
if is_blueprint:
|
||||
password = validated_data.pop("password", None)
|
||||
password_hash = validated_data.pop("password_hash", None)
|
||||
permissions = validated_data.pop("permissions", [])
|
||||
self._validate_password_inputs(password, password_hash)
|
||||
|
||||
"""If this serializer is used in the blueprint context, we allow for
|
||||
directly setting a password. However should be done via the `set_password`
|
||||
method instead of directly setting it like rest_framework."""
|
||||
password = validated_data.pop("password", None)
|
||||
perms_qs = Permission.objects.filter(
|
||||
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
|
||||
).values_list("content_type__app_label", "codename")
|
||||
perms_list = [f"{ct}.{name}" for ct, name in list(perms_qs)]
|
||||
instance: User = super().create(validated_data)
|
||||
if is_blueprint:
|
||||
self._set_password(instance, password, password_hash)
|
||||
perms_qs = Permission.objects.filter(
|
||||
codename__in=[permission.split(".")[1] for permission in permissions]
|
||||
).values_list("content_type__app_label", "codename")
|
||||
perms_list = [f"{ct}.{name}" for ct, name in perms_qs]
|
||||
instance.assign_perms_to_managed_role(perms_list)
|
||||
self._ensure_password_not_empty(instance)
|
||||
self._set_password(instance, password)
|
||||
instance.assign_perms_to_managed_role(perms_list)
|
||||
return instance
|
||||
|
||||
def update(self, instance: User, validated_data: dict) -> User:
|
||||
"""Update a user, with blueprint-only password and permission writes."""
|
||||
is_blueprint = SERIALIZER_CONTEXT_BLUEPRINT in self.context
|
||||
if is_blueprint:
|
||||
password = validated_data.pop("password", None)
|
||||
password_hash = validated_data.pop("password_hash", None)
|
||||
permissions = validated_data.pop("permissions", [])
|
||||
self._validate_password_inputs(password, password_hash)
|
||||
|
||||
"""Same as `create` above, set the password directly if we're in a blueprint
|
||||
context"""
|
||||
password = validated_data.pop("password", None)
|
||||
perms_qs = Permission.objects.filter(
|
||||
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
|
||||
).values_list("content_type__app_label", "codename")
|
||||
perms_list = [f"{ct}.{name}" for ct, name in list(perms_qs)]
|
||||
instance = super().update(instance, validated_data)
|
||||
if is_blueprint:
|
||||
self._set_password(instance, password, password_hash)
|
||||
perms_qs = Permission.objects.filter(
|
||||
codename__in=[permission.split(".")[1] for permission in permissions]
|
||||
).values_list("content_type__app_label", "codename")
|
||||
perms_list = [f"{ct}.{name}" for ct, name in perms_qs]
|
||||
instance.assign_perms_to_managed_role(perms_list)
|
||||
self._ensure_password_not_empty(instance)
|
||||
self._set_password(instance, password)
|
||||
instance.assign_perms_to_managed_role(perms_list)
|
||||
return instance
|
||||
|
||||
def _validate_password_inputs(self, password: str | None, password_hash: str | None):
|
||||
"""Validate mutually-exclusive password inputs before any model mutation."""
|
||||
if password is not None and password_hash is not None:
|
||||
raise ValidationError(_("Cannot set both password and password_hash. Use only one."))
|
||||
if password_hash is None:
|
||||
return
|
||||
try:
|
||||
User.validate_password_hash(password_hash)
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("Failed to identify password hash format", exc_info=exc)
|
||||
raise ValidationError(INVALID_PASSWORD_HASH_MESSAGE) from exc
|
||||
|
||||
def _set_password(self, instance: User, password: str | None, password_hash: str | None = None):
|
||||
"""Set password from plain text or hash."""
|
||||
if password_hash is not None:
|
||||
instance.set_password_from_hash(password_hash)
|
||||
instance.save()
|
||||
elif password:
|
||||
def _set_password(self, instance: User, password: str | None):
|
||||
"""Set password of user if we're in a blueprint context, and if it's an empty
|
||||
string then use an unusable password"""
|
||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and password:
|
||||
instance.set_password(password)
|
||||
instance.save()
|
||||
|
||||
def _ensure_password_not_empty(self, instance: User):
|
||||
"""Store an explicit unusable password instead of an empty password field."""
|
||||
if len(instance.password) == 0:
|
||||
instance.set_unusable_password()
|
||||
instance.save()
|
||||
@@ -436,12 +390,6 @@ class UserPasswordSetSerializer(PassiveSerializer):
|
||||
password = CharField(required=True)
|
||||
|
||||
|
||||
class UserPasswordHashSetSerializer(PassiveSerializer):
|
||||
"""Payload to set a users' password hash directly"""
|
||||
|
||||
password = CharField(required=True)
|
||||
|
||||
|
||||
class UserServiceAccountSerializer(PassiveSerializer):
|
||||
"""Payload to create a service account"""
|
||||
|
||||
@@ -593,30 +541,10 @@ class UserViewSet(
|
||||
|
||||
def get_queryset(self):
|
||||
base_qs = User.objects.all().exclude_anonymous()
|
||||
# Always prefetch groups since group PKs are always serialized.
|
||||
# Use full prefetch when include_groups=true (for groups_obj), ID-only otherwise.
|
||||
if self.serializer_class(context={"request": self.request})._should_include_groups:
|
||||
base_qs = base_qs.prefetch_related("groups")
|
||||
else:
|
||||
base_qs = base_qs.prefetch_related(
|
||||
Prefetch("groups", queryset=Group.objects.all().only("group_uuid"))
|
||||
)
|
||||
if self.serializer_class(context={"request": self.request})._should_include_roles:
|
||||
base_qs = base_qs.prefetch_related("roles")
|
||||
else:
|
||||
base_qs = base_qs.prefetch_related(
|
||||
Prefetch("roles", queryset=Role.objects.all().only("uuid"))
|
||||
)
|
||||
# Annotate is_superuser to avoid N+1 query per user
|
||||
base_qs = base_qs.annotate(
|
||||
_annotated_is_superuser=Exists(
|
||||
Group.objects.filter(
|
||||
is_superuser=True,
|
||||
).filter(
|
||||
Q(users=OuterRef("pk")) | Q(descendant_nodes__descendant__users=OuterRef("pk"))
|
||||
)
|
||||
)
|
||||
)
|
||||
return base_qs
|
||||
|
||||
@extend_schema(
|
||||
@@ -785,11 +713,6 @@ class UserViewSet(
|
||||
self.request.session.modified = True
|
||||
return Response(serializer.initial_data)
|
||||
|
||||
def _update_session_hash_after_password_change(self, request: Request, user: User):
|
||||
if user.pk == request.user.pk and SESSION_KEY_IMPERSONATE_USER not in self.request.session:
|
||||
LOGGER.debug("Updating session hash after password change")
|
||||
update_session_auth_hash(self.request, user)
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
request=UserPasswordSetSerializer,
|
||||
@@ -813,45 +736,9 @@ class UserViewSet(
|
||||
except (ValidationError, IntegrityError) as exc:
|
||||
LOGGER.debug("Failed to set password", exc=exc)
|
||||
return Response(status=400)
|
||||
self._update_session_hash_after_password_change(request, user)
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
request=UserPasswordHashSetSerializer,
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully changed password"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
},
|
||||
)
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["POST"],
|
||||
permission_classes=[IsAuthenticated],
|
||||
)
|
||||
@validate(UserPasswordHashSetSerializer)
|
||||
def set_password_hash(
|
||||
self, request: Request, pk: int, body: UserPasswordHashSetSerializer
|
||||
) -> Response:
|
||||
"""Set a user's password from a pre-hashed Django password value.
|
||||
|
||||
Submit the Django password hash in the shared ``password`` request field.
|
||||
|
||||
This updates authentik's local password verifier only. It does not attempt
|
||||
to propagate the password change to LDAP or Kerberos because no raw password
|
||||
is available from the request payload.
|
||||
"""
|
||||
user: User = self.get_object()
|
||||
try:
|
||||
user.set_password_from_hash(body.validated_data["password"], request=request)
|
||||
user.save()
|
||||
except ValueError as exc:
|
||||
LOGGER.debug("Failed to set password hash", exc=exc)
|
||||
return Response(data={"password": [INVALID_PASSWORD_HASH_MESSAGE]}, status=400)
|
||||
except (ValidationError, IntegrityError) as exc:
|
||||
LOGGER.debug("Failed to set password hash", exc=exc)
|
||||
return Response(status=400)
|
||||
self._update_session_hash_after_password_change(request, user)
|
||||
if user.pk == request.user.pk and SESSION_KEY_IMPERSONATE_USER not in self.request.session:
|
||||
LOGGER.debug("Updating session hash after password change")
|
||||
update_session_auth_hash(self.request, user)
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
"""Hash password using Django's password hashers"""
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Hash a password using Django's password hashers"""
|
||||
|
||||
help = "Hash a password for use with AUTHENTIK_BOOTSTRAP_PASSWORD_HASH"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"password",
|
||||
type=str,
|
||||
help="Password to hash",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
password = options["password"]
|
||||
|
||||
if not password:
|
||||
raise CommandError("Password cannot be empty")
|
||||
try:
|
||||
hashed = make_password(password)
|
||||
self.stdout.write(hashed)
|
||||
except ValueError as exc:
|
||||
raise CommandError(f"Error hashing password: {exc}") from exc
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 5.2.12 on 2026-04-09 18:04
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def migrate_blank_launch_url(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Application = apps.get_model("authentik_core", "Application")
|
||||
|
||||
Application.objects.using(db_alias).filter(meta_launch_url="blank://blank").update(
|
||||
meta_hide=True, meta_launch_url=""
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0058_setup"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="meta_hide",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Hide this application from the user's My applications page.",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_blank_launch_url, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -10,7 +10,7 @@ from uuid import uuid4
|
||||
|
||||
import pgtrigger
|
||||
from deepmerge import always_merger
|
||||
from django.contrib.auth.hashers import check_password, identify_hasher
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.models import AbstractUser, Permission
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
from django.contrib.sessions.base_session import AbstractBaseSession
|
||||
@@ -560,33 +560,6 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
self.password_change_date = now()
|
||||
return super().set_password(raw_password)
|
||||
|
||||
@staticmethod
|
||||
def validate_password_hash(password_hash: str):
|
||||
"""Validate that the value is a recognized Django password hash."""
|
||||
identify_hasher(password_hash) # Raises ValueError if invalid
|
||||
|
||||
def set_password_from_hash(self, password_hash: str, signal=True, sender=None, request=None):
|
||||
"""Set password directly from a pre-hashed value.
|
||||
|
||||
Unlike set_password(), this does not hash the input again. The provided value
|
||||
must already be a valid Django password hash, and it is stored directly on the
|
||||
user after validation.
|
||||
|
||||
Because no raw password is available, downstream password sync integrations
|
||||
such as LDAP and Kerberos cannot be updated from this code path.
|
||||
|
||||
Raises ValueError if the hash format is not recognized.
|
||||
"""
|
||||
self.validate_password_hash(password_hash)
|
||||
if self.pk and signal:
|
||||
from authentik.core.signals import password_hash_changed
|
||||
|
||||
if not sender:
|
||||
sender = self
|
||||
password_hash_changed.send(sender=sender, user=self, request=request)
|
||||
self.password = password_hash
|
||||
self.password_change_date = now()
|
||||
|
||||
def check_password(self, raw_password: str) -> bool:
|
||||
"""
|
||||
Return a boolean of whether the raw_password was correct. Handles
|
||||
@@ -762,9 +735,6 @@ class Application(SerializerModel, PolicyBindingModel):
|
||||
meta_icon = FileField(default="", blank=True)
|
||||
meta_description = models.TextField(default="", blank=True)
|
||||
meta_publisher = models.TextField(default="", blank=True)
|
||||
meta_hide = models.BooleanField(
|
||||
default=False, help_text=_("Hide this application from the user's My applications page.")
|
||||
)
|
||||
|
||||
objects = ApplicationQuerySet.as_manager()
|
||||
|
||||
|
||||
@@ -16,11 +16,7 @@ LOGGER = get_logger()
|
||||
|
||||
@receiver(post_startup)
|
||||
def post_startup_setup_bootstrap(sender, **_):
|
||||
if (
|
||||
not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD")
|
||||
and not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD_HASH")
|
||||
and not getenv("AUTHENTIK_BOOTSTRAP_TOKEN")
|
||||
):
|
||||
if not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD") and not getenv("AUTHENTIK_BOOTSTRAP_TOKEN"):
|
||||
return
|
||||
LOGGER.info("Configuring authentik through bootstrap environment variables")
|
||||
content = BlueprintInstance(path=BOOTSTRAP_BLUEPRINT).retrieve()
|
||||
|
||||
@@ -24,8 +24,6 @@ from authentik.root.ws.consumer import build_device_group
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
# Arguments: user: User, request: HttpRequest | None
|
||||
password_hash_changed = Signal()
|
||||
# Arguments: credentials: dict[str, any], request: HttpRequest,
|
||||
# stage: Stage, context: dict[str, any]
|
||||
login_failed = Signal()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<script data-id="authentik-config">
|
||||
<script data-id="authentik-config" nonce="{{ csp_nonce }}">
|
||||
"use strict";
|
||||
|
||||
window.authentik = {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
|
||||
<meta name="darkreader-lock">
|
||||
<script nonce="{{ csp_nonce }}">window.litNonce = "{{ csp_nonce }}";</script>
|
||||
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
||||
<link rel="icon" href="{{ brand.branding_favicon_url }}">
|
||||
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
||||
@@ -27,7 +28,7 @@
|
||||
|
||||
{% include "base/theme.html" %}
|
||||
|
||||
<style data-id="brand-css">{{ brand_css }}</style>
|
||||
<style data-id="brand-css" nonce="{{ csp_nonce }}">{{ brand_css }}</style>
|
||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
{% else %}
|
||||
<script data-id="theme-script">
|
||||
<script data-id="theme-script" nonce="{{ csp_nonce }}">
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
@@ -22,7 +22,7 @@
|
||||
if (!(["auto", "light", "dark"].includes(locallyStoredTheme))) {
|
||||
locallyStoredTheme = null;
|
||||
}
|
||||
|
||||
|
||||
const initialThemeChoice =
|
||||
new URLSearchParams(window.location.search).get("theme") || locallyStoredTheme;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style data-id="static-styles">
|
||||
<style data-id="static-styles" nonce="{{ csp_nonce }}">
|
||||
:root {
|
||||
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||
}
|
||||
|
||||
@@ -129,7 +129,6 @@ class TestApplicationsAPI(APITestCase):
|
||||
"meta_icon_url": None,
|
||||
"meta_icon_themed_urls": None,
|
||||
"meta_description": "",
|
||||
"meta_hide": False,
|
||||
"meta_publisher": "",
|
||||
"policy_engine_mode": "any",
|
||||
},
|
||||
@@ -188,14 +187,12 @@ class TestApplicationsAPI(APITestCase):
|
||||
"meta_icon_url": None,
|
||||
"meta_icon_themed_urls": None,
|
||||
"meta_description": "",
|
||||
"meta_hide": False,
|
||||
"meta_publisher": "",
|
||||
"policy_engine_mode": "any",
|
||||
},
|
||||
{
|
||||
"launch_url": None,
|
||||
"meta_description": "",
|
||||
"meta_hide": False,
|
||||
"meta_icon": "",
|
||||
"meta_icon_url": None,
|
||||
"meta_icon_themed_urls": None,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
"""Tests for hash_password management command."""
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestHashPasswordCommand(TestCase):
|
||||
"""Test hash_password management command."""
|
||||
|
||||
def test_hash_password(self):
|
||||
"""Test hashing a password."""
|
||||
out = StringIO()
|
||||
call_command("hash_password", "test123", stdout=out)
|
||||
hashed = out.getvalue().strip()
|
||||
|
||||
self.assertTrue(hashed.startswith("pbkdf2_sha256$"))
|
||||
self.assertTrue(check_password("test123", hashed))
|
||||
|
||||
def test_hash_password_empty_fails(self):
|
||||
"""Test that empty password raises error."""
|
||||
with self.assertRaises(CommandError) as ctx:
|
||||
call_command("hash_password", "")
|
||||
|
||||
self.assertIn("Password cannot be empty", str(ctx.exception))
|
||||
@@ -1,7 +1,6 @@
|
||||
from http import HTTPStatus
|
||||
from os import environ
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
@@ -17,7 +16,6 @@ from authentik.tenants.flags import patch_flag
|
||||
class TestSetup(FlowTestCase):
|
||||
def tearDown(self):
|
||||
environ.pop("AUTHENTIK_BOOTSTRAP_PASSWORD", None)
|
||||
environ.pop("AUTHENTIK_BOOTSTRAP_PASSWORD_HASH", None)
|
||||
environ.pop("AUTHENTIK_BOOTSTRAP_TOKEN", None)
|
||||
|
||||
@patch_flag(Setup, True)
|
||||
@@ -156,19 +154,3 @@ class TestSetup(FlowTestCase):
|
||||
token = Token.objects.filter(identifier="authentik-bootstrap-token").first()
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.key, environ["AUTHENTIK_BOOTSTRAP_TOKEN"])
|
||||
|
||||
def test_setup_bootstrap_env_password_hash(self):
|
||||
"""Test setup with password hash env var"""
|
||||
User.objects.filter(username="akadmin").delete()
|
||||
Setup.set(False)
|
||||
|
||||
password = generate_id()
|
||||
password_hash = make_password(password)
|
||||
environ["AUTHENTIK_BOOTSTRAP_PASSWORD_HASH"] = password_hash
|
||||
pre_startup.send(sender=self)
|
||||
post_startup.send(sender=self)
|
||||
|
||||
self.assertTrue(Setup.get())
|
||||
user = User.objects.get(username="akadmin")
|
||||
self.assertEqual(user.password, password_hash)
|
||||
self.assertTrue(user.check_password(password))
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
"""user tests"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.test.testcases import TestCase
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.core.signals import password_changed, password_hash_changed
|
||||
from authentik.events.models import Event
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
@@ -40,99 +33,3 @@ class TestUsers(TestCase):
|
||||
self.assertEqual(Event.objects.count(), 1)
|
||||
user.ak_groups.all()
|
||||
self.assertEqual(Event.objects.count(), 1)
|
||||
|
||||
def test_set_password_from_hash_signal_skips_source_sync_receivers(self):
|
||||
"""Test hash password updates do not expose a raw password to sync receivers."""
|
||||
user = User.objects.create(
|
||||
username=generate_id(),
|
||||
attributes={"distinguishedName": "cn=test,ou=users,dc=example,dc=com"},
|
||||
)
|
||||
password_changed_captured = []
|
||||
password_hash_changed_captured = []
|
||||
dispatch_uid = generate_id()
|
||||
hash_dispatch_uid = generate_id()
|
||||
|
||||
def password_changed_receiver(sender, **kwargs):
|
||||
password_changed_captured.append(kwargs)
|
||||
|
||||
def password_hash_changed_receiver(sender, **kwargs):
|
||||
password_hash_changed_captured.append(kwargs)
|
||||
|
||||
password_changed.connect(password_changed_receiver, dispatch_uid=dispatch_uid)
|
||||
password_hash_changed.connect(
|
||||
password_hash_changed_receiver, dispatch_uid=hash_dispatch_uid
|
||||
)
|
||||
try:
|
||||
with (
|
||||
patch(
|
||||
"authentik.sources.ldap.signals.LDAPSource.objects.filter"
|
||||
) as ldap_sources_filter,
|
||||
patch(
|
||||
"authentik.sources.kerberos.signals."
|
||||
"UserKerberosSourceConnection.objects.select_related"
|
||||
) as kerberos_connections_select,
|
||||
):
|
||||
user.set_password_from_hash(make_password("new-password")) # nosec
|
||||
user.save()
|
||||
finally:
|
||||
password_changed.disconnect(dispatch_uid=dispatch_uid)
|
||||
password_hash_changed.disconnect(dispatch_uid=hash_dispatch_uid)
|
||||
|
||||
self.assertEqual(password_changed_captured, [])
|
||||
self.assertEqual(len(password_hash_changed_captured), 1)
|
||||
ldap_sources_filter.assert_not_called()
|
||||
kerberos_connections_select.assert_not_called()
|
||||
|
||||
|
||||
class TestUserSerializerPasswordHash(TestCase):
|
||||
"""Test UserSerializer password_hash support in blueprint context."""
|
||||
|
||||
def test_password_hash_sets_password_directly(self):
|
||||
"""Test a valid password hash is stored without re-hashing."""
|
||||
password = "test-password-123" # nosec
|
||||
password_hash = make_password(password)
|
||||
serializer = UserSerializer(
|
||||
data={
|
||||
"username": generate_id(),
|
||||
"name": "Test User",
|
||||
"password_hash": password_hash,
|
||||
},
|
||||
context={SERIALIZER_CONTEXT_BLUEPRINT: True},
|
||||
)
|
||||
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
user = serializer.save()
|
||||
|
||||
self.assertEqual(user.password, password_hash)
|
||||
self.assertTrue(user.check_password(password))
|
||||
self.assertIsNotNone(user.password_change_date)
|
||||
|
||||
def test_password_hash_rejects_invalid_format(self):
|
||||
"""Test invalid password hash values are rejected."""
|
||||
serializer = UserSerializer(
|
||||
data={
|
||||
"username": generate_id(),
|
||||
"name": "Test User",
|
||||
"password_hash": "not-a-valid-hash",
|
||||
},
|
||||
context={SERIALIZER_CONTEXT_BLUEPRINT: True},
|
||||
)
|
||||
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
serializer.save()
|
||||
|
||||
self.assertIn("Invalid password hash format", str(ctx.exception))
|
||||
|
||||
def test_password_hash_ignored_outside_blueprint_context(self):
|
||||
"""Test password_hash is not accepted by the regular serializer."""
|
||||
serializer = UserSerializer(
|
||||
data={
|
||||
"username": generate_id(),
|
||||
"name": "Test User",
|
||||
"password_hash": make_password("test"), # nosec
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
self.assertNotIn("password_hash", serializer.validated_data)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from json import loads
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.urls.base import reverse
|
||||
from django.utils.timezone import now
|
||||
from rest_framework.test import APITestCase
|
||||
@@ -27,9 +26,6 @@ from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignatio
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
INVALID_PASSWORD_HASH = "not-a-valid-hash"
|
||||
INVALID_PASSWORD_HASH_ERROR = "Invalid password hash format. Must be a valid Django password hash."
|
||||
|
||||
|
||||
class TestUsersAPI(APITestCase):
|
||||
"""Test Users API"""
|
||||
@@ -38,20 +34,6 @@ class TestUsersAPI(APITestCase):
|
||||
self.admin = create_test_admin_user()
|
||||
self.user = create_test_user()
|
||||
|
||||
def _set_password_hash(self, user: User, password_hash: str, client=None):
|
||||
return (client or self.client).post(
|
||||
reverse("authentik_api:user-set-password-hash", kwargs={"pk": user.pk}),
|
||||
data={"password": password_hash},
|
||||
)
|
||||
|
||||
def _assert_password_hash_set(
|
||||
self, user: User, password: str, password_hash: str, response
|
||||
) -> None:
|
||||
self.assertEqual(response.status_code, 204, response.data)
|
||||
user.refresh_from_db()
|
||||
self.assertEqual(user.password, password_hash)
|
||||
self.assertTrue(user.check_password(password))
|
||||
|
||||
def test_filter_type(self):
|
||||
"""Test API filtering by type"""
|
||||
self.client.force_login(self.admin)
|
||||
@@ -131,26 +113,6 @@ class TestUsersAPI(APITestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]})
|
||||
|
||||
def test_set_password_hash(self):
|
||||
"""Test setting a user's password from a hash."""
|
||||
self.client.force_login(self.admin)
|
||||
password = generate_key()
|
||||
password_hash = make_password(password)
|
||||
response = self._set_password_hash(self.user, password_hash)
|
||||
|
||||
self._assert_password_hash_set(self.user, password, password_hash, response)
|
||||
|
||||
def test_set_password_hash_invalid(self):
|
||||
"""Test invalid password hashes are rejected."""
|
||||
self.client.force_login(self.admin)
|
||||
response = self._set_password_hash(self.user, INVALID_PASSWORD_HASH)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
{"password": [INVALID_PASSWORD_HASH_ERROR]},
|
||||
)
|
||||
|
||||
def test_recovery(self):
|
||||
"""Test user recovery link"""
|
||||
flow = create_test_flow(
|
||||
@@ -299,29 +261,6 @@ class TestUsersAPI(APITestCase):
|
||||
self.assertTrue(token_filter.exists())
|
||||
self.assertTrue(token_filter.first().expiring)
|
||||
|
||||
def test_service_account_set_password_hash(self):
|
||||
"""Service account password hash can be set through the API."""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-service-account"),
|
||||
data={
|
||||
"name": "test-sa",
|
||||
"create_group": False,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.data)
|
||||
body = loads(response.content)
|
||||
|
||||
user = User.objects.get(pk=body["user_pk"])
|
||||
self.assertEqual(user.type, UserTypes.SERVICE_ACCOUNT)
|
||||
self.assertFalse(user.has_usable_password())
|
||||
|
||||
password = generate_key()
|
||||
password_hash = make_password(password)
|
||||
response = self._set_password_hash(user, password_hash)
|
||||
|
||||
self._assert_password_hash_set(user, password, password_hash, response)
|
||||
|
||||
def test_service_account_no_expire(self):
|
||||
"""Service account creation without token expiration"""
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
@@ -12,7 +12,7 @@ from authentik.core.models import (
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.signals import password_changed, password_hash_changed
|
||||
from authentik.core.signals import password_changed
|
||||
from authentik.enterprise.providers.ssf.models import (
|
||||
EventTypes,
|
||||
SSFProvider,
|
||||
@@ -84,13 +84,14 @@ def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSessi
|
||||
)
|
||||
|
||||
|
||||
def _send_password_credential_change(user: User, change_type: str):
|
||||
@receiver(password_changed)
|
||||
def ssf_password_changed_cred_change(sender, user: User, password: str | None, **_):
|
||||
"""Credential change trigger (password changed)"""
|
||||
send_ssf_events(
|
||||
EventTypes.CAEP_CREDENTIAL_CHANGE,
|
||||
{
|
||||
"credential_type": "password",
|
||||
"change_type": change_type,
|
||||
"change_type": "revoke" if password is None else "update",
|
||||
},
|
||||
sub_id={
|
||||
"format": "complex",
|
||||
@@ -102,16 +103,6 @@ def _send_password_credential_change(user: User, change_type: str):
|
||||
)
|
||||
|
||||
|
||||
@receiver(password_hash_changed)
|
||||
@receiver(password_changed)
|
||||
def ssf_password_changed_cred_change(signal, sender, user: User, password: str | None = None, **_):
|
||||
"""Credential change trigger (password changed)"""
|
||||
if signal is password_hash_changed:
|
||||
_send_password_credential_change(user, "update")
|
||||
return
|
||||
_send_password_credential_change(user, "revoke" if password is None else "update")
|
||||
|
||||
|
||||
device_type_map = {
|
||||
StaticDevice: "pin",
|
||||
TOTPDevice: "pin",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
@@ -53,21 +52,6 @@ class TestSignals(APITestCase):
|
||||
)
|
||||
self.assertEqual(res.status_code, 201, res.content)
|
||||
|
||||
def _assert_password_credential_change(self, user, change_type: str):
|
||||
stream = Stream.objects.filter(provider=self.provider).first()
|
||||
self.assertIsNotNone(stream)
|
||||
event = StreamEvent.objects.filter(stream=stream).first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
|
||||
event_payload = event.payload["events"][
|
||||
"https://schemas.openid.net/secevent/caep/event-type/credential-change"
|
||||
]
|
||||
self.assertEqual(event_payload["change_type"], change_type)
|
||||
self.assertEqual(event_payload["credential_type"], "password")
|
||||
self.assertEqual(event.payload["sub_id"]["format"], "complex")
|
||||
self.assertEqual(event.payload["sub_id"]["user"]["format"], "email")
|
||||
self.assertEqual(event.payload["sub_id"]["user"]["email"], user.email)
|
||||
|
||||
def test_signal_logout(self):
|
||||
"""Test user logout"""
|
||||
user = create_test_user()
|
||||
@@ -95,25 +79,19 @@ class TestSignals(APITestCase):
|
||||
user.set_password(generate_id())
|
||||
user.save()
|
||||
|
||||
self._assert_password_credential_change(user, "update")
|
||||
|
||||
def test_signal_password_change_from_hash(self):
|
||||
"""Test user password change from a pre-hashed password."""
|
||||
user = create_test_user()
|
||||
self.client.force_login(user)
|
||||
user.set_password_from_hash(make_password(generate_id()))
|
||||
user.save()
|
||||
|
||||
self._assert_password_credential_change(user, "update")
|
||||
|
||||
def test_signal_password_revoke(self):
|
||||
"""Test explicit password revoke."""
|
||||
user = create_test_user()
|
||||
self.client.force_login(user)
|
||||
user.set_password(None)
|
||||
user.save()
|
||||
|
||||
self._assert_password_credential_change(user, "revoke")
|
||||
stream = Stream.objects.filter(provider=self.provider).first()
|
||||
self.assertIsNotNone(stream)
|
||||
event = StreamEvent.objects.filter(stream=stream).first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
|
||||
event_payload = event.payload["events"][
|
||||
"https://schemas.openid.net/secevent/caep/event-type/credential-change"
|
||||
]
|
||||
self.assertEqual(event_payload["change_type"], "update")
|
||||
self.assertEqual(event_payload["credential_type"], "password")
|
||||
self.assertEqual(event.payload["sub_id"]["format"], "complex")
|
||||
self.assertEqual(event.payload["sub_id"]["user"]["format"], "email")
|
||||
self.assertEqual(event.payload["sub_id"]["user"]["email"], user.email)
|
||||
|
||||
def test_signal_authenticator_added(self):
|
||||
"""Test authenticator creation signal"""
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.http import HttpRequest
|
||||
from rest_framework.request import Request
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
from authentik.core.signals import login_failed, password_changed, password_hash_changed
|
||||
from authentik.core.signals import login_failed, password_changed
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.flows.planner import (
|
||||
@@ -112,15 +112,8 @@ def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_
|
||||
)
|
||||
|
||||
|
||||
@receiver(password_hash_changed)
|
||||
@receiver(password_changed)
|
||||
def on_password_changed(
|
||||
sender,
|
||||
user: User,
|
||||
password: str | None = None,
|
||||
request: HttpRequest | None = None,
|
||||
**_,
|
||||
):
|
||||
def on_password_changed(sender, user: User, password: str, request: HttpRequest | None, **_):
|
||||
"""Log password change"""
|
||||
Event.new(EventAction.PASSWORD_SET).from_http(request, user=user)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.views.debug import SafeExceptionReporterFilter
|
||||
@@ -11,7 +10,7 @@ from guardian.shortcuts import get_anonymous_user
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.models import Event
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views.executor import QS_QUERY, SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
@@ -214,14 +213,3 @@ class TestEvents(TestCase):
|
||||
event = Event.new("unittest", foo="foo bar \u0000 baz")
|
||||
event.save()
|
||||
self.assertEqual(event.context["foo"], "foo bar baz")
|
||||
|
||||
def test_password_set_signal_on_set_password_from_hash(self):
|
||||
"""Changing password from hash should still emit an audit event."""
|
||||
user = create_test_user()
|
||||
old_count = Event.objects.filter(action=EventAction.PASSWORD_SET, user__pk=user.pk).count()
|
||||
|
||||
user.set_password_from_hash(make_password(generate_id()))
|
||||
user.save()
|
||||
|
||||
new_count = Event.objects.filter(action=EventAction.PASSWORD_SET, user__pk=user.pk).count()
|
||||
self.assertEqual(new_count, old_count + 1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<html>
|
||||
<script>
|
||||
<script nonce="{{ csp_nonce }}">
|
||||
window.parent.postMessage({
|
||||
message: "submit",
|
||||
source: "goauthentik.io",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||
<link rel="prefetch" href="{{ flow_background_url }}" />
|
||||
{% include "base/header_js.html" %}
|
||||
<style data-id="flow-sfe">
|
||||
<style data-id="flow-sfe" nonce="{{ csp_nonce }}">
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
@@ -43,13 +43,13 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="{% static 'dist/sfe/index.js' %}"></script>
|
||||
</head>
|
||||
<body class="d-flex align-items-center py-4 bg-body-tertiary">
|
||||
<div class="card m-auto">
|
||||
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
|
||||
</main>
|
||||
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
|
||||
</div>
|
||||
<script src="{% static 'dist/sfe/index.js' %}"></script>
|
||||
<div class="card m-auto">
|
||||
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
|
||||
</main>
|
||||
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% comment %}
|
||||
@see {@link web/types/webcomponents.d.ts} for type definitions.
|
||||
{% endcomment %}
|
||||
<script data-id="shady-dom">
|
||||
<script data-id="shady-dom" nonce="{{ csp_nonce }}">
|
||||
"use strict";
|
||||
|
||||
window.ShadyDOM = window.ShadyDOM || {}
|
||||
@@ -20,7 +20,7 @@
|
||||
</script>
|
||||
{% endif %}
|
||||
{% include "base/header_js.html" %}
|
||||
<script data-id="flow-config">
|
||||
<script data-id="flow-config" nonce="{{ csp_nonce }}">
|
||||
"use strict";
|
||||
|
||||
window.authentik.flow = {
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
{% block head %}
|
||||
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||
<style data-id="flow-css">
|
||||
<style data-id="flow-css" nonce="{{ csp_nonce }}">
|
||||
:root {
|
||||
--ak-global--background-image: url("{{ flow_background_url }}");
|
||||
}
|
||||
@@ -55,10 +55,10 @@
|
||||
loading
|
||||
>
|
||||
{% include "base/placeholder.html" %}
|
||||
|
||||
|
||||
<ak-brand-links name="flow-links" slot="footer"></ak-brand-links>
|
||||
</ak-flow-executor>
|
||||
|
||||
|
||||
<ak-flow-inspector
|
||||
slot="panel"
|
||||
id="flow-inspector"
|
||||
|
||||
@@ -165,6 +165,8 @@ web:
|
||||
timeout_http_read: 30s
|
||||
timeout_http_write: 60s
|
||||
timeout_http_idle: 120s
|
||||
csp:
|
||||
report_only: false
|
||||
|
||||
worker:
|
||||
processes: 1
|
||||
|
||||
@@ -24,11 +24,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.validation import validate
|
||||
from authentik.common.saml.constants import (
|
||||
DEFAULT_ISSUER,
|
||||
SAML_BINDING_POST,
|
||||
SAML_BINDING_REDIRECT,
|
||||
)
|
||||
from authentik.common.saml.constants import SAML_BINDING_POST, SAML_BINDING_REDIRECT
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
|
||||
@@ -59,7 +55,6 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
"""SAMLProvider Serializer"""
|
||||
|
||||
url_download_metadata = SerializerMethodField()
|
||||
url_issuer = SerializerMethodField()
|
||||
|
||||
url_sso_post = SerializerMethodField()
|
||||
url_sso_redirect = SerializerMethodField()
|
||||
@@ -90,23 +85,6 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
+ "?download"
|
||||
)
|
||||
|
||||
def get_url_issuer(self, instance: SAMLProvider) -> str:
|
||||
"""Get Issuer/EntityID URL"""
|
||||
if instance.issuer_override:
|
||||
return instance.issuer_override
|
||||
if "request" not in self._context:
|
||||
return DEFAULT_ISSUER
|
||||
request: HttpRequest = self._context["request"]._request
|
||||
try:
|
||||
return request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_saml:base",
|
||||
kwargs={"application_slug": instance.application.slug},
|
||||
)
|
||||
)
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
return DEFAULT_ISSUER
|
||||
|
||||
def get_url_sso_post(self, instance: SAMLProvider) -> str:
|
||||
"""Get SSO Post URL"""
|
||||
if "request" not in self._context:
|
||||
@@ -220,7 +198,7 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
"acs_url",
|
||||
"sls_url",
|
||||
"audience",
|
||||
"issuer_override",
|
||||
"issuer",
|
||||
"assertion_valid_not_before",
|
||||
"assertion_valid_not_on_or_after",
|
||||
"session_valid_not_on_or_after",
|
||||
@@ -242,7 +220,6 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
"default_relay_state",
|
||||
"default_name_id_policy",
|
||||
"url_download_metadata",
|
||||
"url_issuer",
|
||||
"url_sso_post",
|
||||
"url_sso_redirect",
|
||||
"url_sso_init",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-24 06:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_saml", "0021_samlprovider_sign_logout_response"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="samlprovider",
|
||||
old_name="issuer",
|
||||
new_name="issuer_override",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="issuer_override",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="Also known as EntityID. Providing a value overrides the default issuer generated by authentik.",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="samlsession",
|
||||
name="issuer",
|
||||
field=models.TextField(
|
||||
default=None, help_text="SAML Issuer used for this session", null=True
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -77,14 +77,7 @@ class SAMLProvider(Provider):
|
||||
"no audience restriction will be added."
|
||||
),
|
||||
)
|
||||
issuer_override = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text=_(
|
||||
"Also known as EntityID. Providing a value overrides the default issuer "
|
||||
"generated by authentik."
|
||||
),
|
||||
)
|
||||
issuer = models.TextField(help_text=_("Also known as EntityID"), default="authentik")
|
||||
sls_url = models.TextField(
|
||||
blank=True,
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
||||
@@ -325,9 +318,6 @@ class SAMLSession(InternallyManagedMixin, SerializerModel, ExpiringModel):
|
||||
session_index = models.TextField(help_text=_("SAML SessionIndex for this session"))
|
||||
name_id = models.TextField(help_text=_("SAML NameID value for this session"))
|
||||
name_id_format = models.TextField(default="", blank=True, help_text=_("SAML NameID format"))
|
||||
issuer = models.TextField(
|
||||
default=None, null=True, help_text=_("SAML Issuer used for this session")
|
||||
)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@property
|
||||
|
||||
@@ -6,7 +6,6 @@ from types import GeneratorType
|
||||
|
||||
import xmlsec
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from lxml import etree # nosec
|
||||
from lxml.etree import Element, SubElement, _Element # nosec
|
||||
@@ -64,7 +63,6 @@ class AssertionProcessor:
|
||||
session_index: str
|
||||
name_id: str
|
||||
name_id_format: str
|
||||
issuer: str
|
||||
session_not_on_or_after_datetime: datetime
|
||||
|
||||
def __init__(self, provider: SAMLProvider, request: HttpRequest, auth_n_request: AuthNRequest):
|
||||
@@ -139,24 +137,10 @@ class AssertionProcessor:
|
||||
continue
|
||||
return attribute_statement
|
||||
|
||||
def _get_issuer_value(self) -> str:
|
||||
"""Get issuer value, with fallback to generated URL if empty"""
|
||||
# If user has set an override issuer, use it
|
||||
if self.provider.issuer_override:
|
||||
return self.provider.issuer_override
|
||||
|
||||
return self.http_request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_saml:base",
|
||||
kwargs={"application_slug": self.provider.application.slug},
|
||||
)
|
||||
)
|
||||
|
||||
def get_issuer(self) -> Element:
|
||||
"""Get Issuer Element"""
|
||||
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer", nsmap=NS_MAP)
|
||||
self.issuer = self._get_issuer_value()
|
||||
issuer.text = self.issuer
|
||||
issuer.text = self.provider.issuer
|
||||
return issuer
|
||||
|
||||
def get_assertion_auth_n_statement(self) -> Element:
|
||||
|
||||
@@ -8,7 +8,6 @@ from lxml import etree # nosec
|
||||
from lxml.etree import Element, _Element
|
||||
|
||||
from authentik.common.saml.constants import (
|
||||
DEFAULT_ISSUER,
|
||||
DIGEST_ALGORITHM_TRANSLATION_MAP,
|
||||
NS_MAP,
|
||||
NS_SAML_ASSERTION,
|
||||
@@ -34,12 +33,11 @@ class LogoutRequestProcessor:
|
||||
name_id_format: str
|
||||
session_index: str | None
|
||||
relay_state: str | None
|
||||
issuer: str | None
|
||||
|
||||
_issue_instant: str
|
||||
_request_id: str
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
def __init__(
|
||||
self,
|
||||
provider: SAMLProvider,
|
||||
user: User | None,
|
||||
@@ -48,7 +46,6 @@ class LogoutRequestProcessor:
|
||||
name_id_format: str = SAML_NAME_ID_FORMAT_EMAIL,
|
||||
session_index: str | None = None,
|
||||
relay_state: str | None = None,
|
||||
issuer: str | None = None,
|
||||
):
|
||||
self.provider = provider
|
||||
self.user = user
|
||||
@@ -57,23 +54,14 @@ class LogoutRequestProcessor:
|
||||
self.name_id_format = name_id_format
|
||||
self.session_index = session_index
|
||||
self.relay_state = relay_state
|
||||
self.issuer = issuer
|
||||
|
||||
self._issue_instant = get_time_string()
|
||||
self._request_id = get_random_id()
|
||||
|
||||
def _get_issuer_value(self) -> str:
|
||||
"""Get issuer value from session, with fallback to provider"""
|
||||
if self.issuer:
|
||||
return self.issuer
|
||||
if self.provider.issuer_override:
|
||||
return self.provider.issuer_override
|
||||
return DEFAULT_ISSUER
|
||||
|
||||
def get_issuer(self) -> Element:
|
||||
"""Get Issuer element"""
|
||||
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
|
||||
issuer.text = self._get_issuer_value()
|
||||
issuer.text = self.provider.issuer
|
||||
return issuer
|
||||
|
||||
def get_name_id(self) -> Element:
|
||||
|
||||
@@ -8,7 +8,6 @@ from lxml import etree
|
||||
from lxml.etree import Element, SubElement
|
||||
|
||||
from authentik.common.saml.constants import (
|
||||
DEFAULT_ISSUER,
|
||||
DIGEST_ALGORITHM_TRANSLATION_MAP,
|
||||
NS_MAP,
|
||||
NS_SAML_ASSERTION,
|
||||
@@ -29,38 +28,27 @@ class LogoutResponseProcessor:
|
||||
logout_request: LogoutRequest
|
||||
destination: str | None
|
||||
relay_state: str | None
|
||||
issuer: str | None
|
||||
_issue_instant: str
|
||||
_response_id: str
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
def __init__(
|
||||
self,
|
||||
provider: SAMLProvider,
|
||||
logout_request: LogoutRequest,
|
||||
destination: str | None = None,
|
||||
relay_state: str | None = None,
|
||||
issuer: str | None = None,
|
||||
):
|
||||
self.provider = provider
|
||||
self.logout_request = logout_request
|
||||
self.destination = destination
|
||||
self.relay_state = relay_state or (logout_request.relay_state if logout_request else None)
|
||||
self.issuer = issuer
|
||||
self._issue_instant = get_time_string()
|
||||
self._response_id = get_random_id()
|
||||
|
||||
def _get_issuer_value(self) -> str:
|
||||
"""Get issuer value from session, with fallback to provider"""
|
||||
if self.issuer:
|
||||
return self.issuer
|
||||
if self.provider.issuer_override:
|
||||
return self.provider.issuer_override
|
||||
return DEFAULT_ISSUER
|
||||
|
||||
def get_issuer(self) -> Element:
|
||||
"""Get Issuer element"""
|
||||
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
|
||||
issuer.text = self._get_issuer_value()
|
||||
issuer.text = self.provider.issuer
|
||||
return issuer
|
||||
|
||||
def build(self, status: str = "Success") -> Element:
|
||||
|
||||
@@ -40,19 +40,6 @@ class MetadataProcessor:
|
||||
self.force_binding = None
|
||||
self.xml_id = "_" + sha256(f"{provider.name}-{provider.pk}".encode("ascii")).hexdigest()
|
||||
|
||||
def _get_issuer_value(self) -> str:
|
||||
"""Get issuer value, with fallback to generated URL if empty"""
|
||||
# If user has set an override issuer, use it
|
||||
if self.provider.issuer_override:
|
||||
return self.provider.issuer_override
|
||||
|
||||
return self.http_request.build_absolute_uri(
|
||||
reverse(
|
||||
"authentik_providers_saml:base",
|
||||
kwargs={"application_slug": self.provider.application.slug},
|
||||
)
|
||||
)
|
||||
|
||||
# Using type unions doesn't work with cython types (which is what lxml is)
|
||||
def get_signing_key_descriptor(self) -> Element | None:
|
||||
"""Get Signing KeyDescriptor, if enabled for the provider"""
|
||||
@@ -202,7 +189,7 @@ class MetadataProcessor:
|
||||
"""Build full EntityDescriptor"""
|
||||
entity_descriptor = Element(f"{{{NS_SAML_METADATA}}}EntityDescriptor", nsmap=NS_MAP)
|
||||
entity_descriptor.attrib["ID"] = self.xml_id
|
||||
entity_descriptor.attrib["entityID"] = self._get_issuer_value()
|
||||
entity_descriptor.attrib["entityID"] = self.provider.issuer
|
||||
|
||||
if self.provider.signing_kp:
|
||||
self._prepare_signature(entity_descriptor)
|
||||
|
||||
@@ -51,6 +51,7 @@ class ServiceProviderMetadata:
|
||||
provider = SAMLProvider.objects.create(
|
||||
name=name, authorization_flow=authorization_flow, invalidation_flow=invalidation_flow
|
||||
)
|
||||
provider.issuer = self.entity_id
|
||||
provider.sp_binding = self.acs_binding
|
||||
provider.acs_url = self.acs_location
|
||||
provider.default_name_id_policy = self.name_id_policy
|
||||
|
||||
@@ -75,7 +75,6 @@ def handle_saml_iframe_pre_user_logout(
|
||||
name_id_format=session.name_id_format,
|
||||
session_index=session.session_index,
|
||||
relay_state=relay_state,
|
||||
issuer=session.issuer,
|
||||
)
|
||||
|
||||
if session.provider.sls_binding == SAMLBindings.POST:
|
||||
@@ -164,7 +163,6 @@ def handle_flow_pre_user_logout(
|
||||
name_id_format=session.name_id_format,
|
||||
session_index=session.session_index,
|
||||
relay_state=relay_state,
|
||||
issuer=session.issuer,
|
||||
)
|
||||
|
||||
if session.provider.sls_binding == SAMLBindings.POST:
|
||||
@@ -226,7 +224,6 @@ def user_session_deleted_saml_logout(sender, instance: AuthenticatedSession, **_
|
||||
name_id=saml_session.name_id,
|
||||
name_id_format=saml_session.name_id_format,
|
||||
session_index=saml_session.session_index,
|
||||
issuer=saml_session.issuer,
|
||||
)
|
||||
|
||||
|
||||
@@ -260,5 +257,4 @@ def user_deactivated_saml_logout(sender, instance: User, **kwargs):
|
||||
name_id=saml_session.name_id,
|
||||
name_id_format=saml_session.name_id_format,
|
||||
session_index=saml_session.session_index,
|
||||
issuer=saml_session.issuer,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@ def send_saml_logout_request(
|
||||
name_id: str,
|
||||
name_id_format: str,
|
||||
session_index: str,
|
||||
issuer: str,
|
||||
):
|
||||
"""Send SAML LogoutRequest to a Service Provider using session data"""
|
||||
provider = SAMLProvider.objects.filter(pk=provider_pk).first()
|
||||
@@ -48,7 +47,6 @@ def send_saml_logout_request(
|
||||
name_id=name_id,
|
||||
name_id_format=name_id_format,
|
||||
session_index=session_index,
|
||||
issuer=issuer,
|
||||
)
|
||||
|
||||
return send_post_logout_request(provider, processor)
|
||||
@@ -91,7 +89,6 @@ def send_saml_logout_response(
|
||||
sls_url: str,
|
||||
logout_request_id: str | None = None,
|
||||
relay_state: str | None = None,
|
||||
issuer: str | None = None,
|
||||
):
|
||||
"""Send SAML LogoutResponse to a Service Provider using backchannel (server-to-server)"""
|
||||
provider = SAMLProvider.objects.filter(pk=provider_pk).first()
|
||||
@@ -122,7 +119,6 @@ def send_saml_logout_response(
|
||||
logout_request=logout_request,
|
||||
destination=sls_url,
|
||||
relay_state=relay_state,
|
||||
issuer=issuer,
|
||||
)
|
||||
|
||||
encoded_response = processor.encode_post()
|
||||
|
||||
@@ -15,7 +15,6 @@ from authentik.common.saml.constants import (
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||
)
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import (
|
||||
RequestFactory,
|
||||
create_test_admin_user,
|
||||
@@ -98,11 +97,6 @@ class TestAuthNRequest(TestCase):
|
||||
)
|
||||
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
self.provider.save()
|
||||
Application.objects.create(
|
||||
name="test-app",
|
||||
slug="test-app",
|
||||
provider=self.provider,
|
||||
)
|
||||
self.source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
@@ -532,7 +526,7 @@ class TestAuthNRequest(TestCase):
|
||||
authorization_flow=create_test_flow(),
|
||||
acs_url="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
audience="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
issuer_override="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
issuer="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
signing_kp=static_keypair,
|
||||
verification_kp=static_keypair,
|
||||
)
|
||||
@@ -553,7 +547,7 @@ class TestAuthNRequest(TestCase):
|
||||
"saml/acs/2d737f96-55fb-4035-953e-5e24134eb778"
|
||||
),
|
||||
audience="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
issuer_override="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
issuer="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
signing_kp=create_test_cert(),
|
||||
)
|
||||
parsed_request = AuthNRequestParser(provider).parse(POST_REQUEST)
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestNativeLogoutStageView(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp1.example.com/acs",
|
||||
sls_url="https://sp1.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="redirect",
|
||||
sls_binding="redirect",
|
||||
logout_method=SAMLLogoutMethods.FRONTCHANNEL_NATIVE,
|
||||
@@ -58,7 +58,7 @@ class TestNativeLogoutStageView(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp2.example.com/acs",
|
||||
sls_url="https://sp2.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="post",
|
||||
sls_binding="post",
|
||||
logout_method=SAMLLogoutMethods.FRONTCHANNEL_NATIVE,
|
||||
@@ -218,7 +218,7 @@ class TestIframeLogoutStageView(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp1.example.com/acs",
|
||||
sls_url="https://sp1.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="redirect",
|
||||
sls_binding="redirect",
|
||||
logout_method="frontchannel_iframe",
|
||||
@@ -229,7 +229,7 @@ class TestIframeLogoutStageView(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp2.example.com/acs",
|
||||
sls_url="https://sp2.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="post",
|
||||
sls_binding="post",
|
||||
logout_method="frontchannel_iframe",
|
||||
@@ -372,7 +372,7 @@ class TestIdPLogoutIntegration(FlowTestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="https://sp.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="redirect",
|
||||
sls_binding="redirect",
|
||||
signing_kp=self.keypair,
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestLogoutIntegration(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="https://sp.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="redirect",
|
||||
sls_binding="redirect",
|
||||
signature_algorithm=RSA_SHA256,
|
||||
@@ -57,7 +57,7 @@ class TestLogoutIntegration(TestCase):
|
||||
parsed = self.parser.parse(encoded)
|
||||
|
||||
# Verify all fields match
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer)
|
||||
self.assertEqual(parsed.name_id, "test@example.com")
|
||||
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
|
||||
self.assertEqual(parsed.session_index, "test-session-123")
|
||||
@@ -72,7 +72,7 @@ class TestLogoutIntegration(TestCase):
|
||||
parsed = self.parser.parse_detached(encoded)
|
||||
|
||||
# Verify all fields match
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer)
|
||||
self.assertEqual(parsed.name_id, "test@example.com")
|
||||
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
|
||||
self.assertEqual(parsed.session_index, "test-session-123")
|
||||
@@ -106,7 +106,7 @@ class TestLogoutIntegration(TestCase):
|
||||
parsed = parser.parse(encoded)
|
||||
|
||||
# Verify all fields match
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer)
|
||||
self.assertEqual(parsed.name_id, "signed@example.com")
|
||||
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
|
||||
self.assertEqual(parsed.session_index, "signed-session-456")
|
||||
@@ -125,7 +125,7 @@ class TestLogoutIntegration(TestCase):
|
||||
parsed = self.parser.parse_detached(saml_request)
|
||||
|
||||
# Verify parsing succeeded
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer)
|
||||
self.assertEqual(parsed.name_id, "test@example.com")
|
||||
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
|
||||
|
||||
@@ -164,7 +164,7 @@ class TestLogoutIntegration(TestCase):
|
||||
|
||||
# Parse the SAMLRequest (unsigned XML)
|
||||
parsed = self.parser.parse_detached(params["SAMLRequest"][0])
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer)
|
||||
|
||||
def test_form_data_can_be_parsed(self):
|
||||
"""Test that form data generates parseable POST request"""
|
||||
@@ -175,7 +175,7 @@ class TestLogoutIntegration(TestCase):
|
||||
parsed = self.parser.parse(form_data["SAMLRequest"])
|
||||
|
||||
# Verify parsing succeeded
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(parsed.issuer, self.provider.issuer)
|
||||
self.assertEqual(parsed.name_id, "test@example.com")
|
||||
self.assertEqual(parsed.name_id_format, SAML_NAME_ID_FORMAT_EMAIL)
|
||||
self.assertEqual(parsed.session_index, "test-session-123")
|
||||
@@ -244,4 +244,4 @@ class TestLogoutIntegration(TestCase):
|
||||
|
||||
# But same issuer
|
||||
self.assertEqual(parsed1.issuer, parsed2.issuer)
|
||||
self.assertEqual(parsed1.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(parsed1.issuer, self.provider.issuer)
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestLogoutRequestProcessor(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="https://sp.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="redirect",
|
||||
sls_binding="redirect",
|
||||
signature_algorithm=RSA_SHA256,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""logout response tests"""
|
||||
|
||||
from defusedxml import ElementTree
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.common.saml.constants import (
|
||||
@@ -9,13 +9,10 @@ from authentik.common.saml.constants import (
|
||||
NS_SAML_PROTOCOL,
|
||||
NS_SIGNATURE,
|
||||
)
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
|
||||
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
|
||||
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
||||
|
||||
|
||||
class TestLogoutResponse(TestCase):
|
||||
@@ -24,7 +21,6 @@ class TestLogoutResponse(TestCase):
|
||||
@apply_blueprint("system/providers-saml.yaml")
|
||||
def setUp(self):
|
||||
cert = create_test_cert()
|
||||
self.factory = RequestFactory()
|
||||
self.provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
authorization_flow=create_test_flow(),
|
||||
acs_url="http://testserver/source/saml/provider/acs/",
|
||||
@@ -34,31 +30,17 @@ class TestLogoutResponse(TestCase):
|
||||
)
|
||||
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
self.provider.save()
|
||||
self.application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
provider=self.provider,
|
||||
)
|
||||
|
||||
def test_build_response(self):
|
||||
"""Test building a LogoutResponse uses the generated issuer from the assertion"""
|
||||
# Generate the issuer the same way the assertion/metadata processors would
|
||||
request = self.factory.get("/")
|
||||
metadata_processor = MetadataProcessor(self.provider, request)
|
||||
generated_issuer = metadata_processor._get_issuer_value()
|
||||
|
||||
"""Test building a LogoutResponse"""
|
||||
logout_request = LogoutRequest(
|
||||
id="test-request-id",
|
||||
issuer="test-sp",
|
||||
relay_state="test-relay-state",
|
||||
)
|
||||
|
||||
# Pass the generated issuer as if it came from SAMLSession.issuer
|
||||
processor = LogoutResponseProcessor(
|
||||
self.provider,
|
||||
logout_request,
|
||||
destination=self.provider.sls_url,
|
||||
issuer=generated_issuer,
|
||||
self.provider, logout_request, destination=self.provider.sls_url
|
||||
)
|
||||
response_xml = processor.build_response(status="Success")
|
||||
|
||||
@@ -69,9 +51,9 @@ class TestLogoutResponse(TestCase):
|
||||
self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
|
||||
self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
|
||||
|
||||
# Check Issuer matches the generated issuer from the assertion processor
|
||||
# Check Issuer
|
||||
issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
|
||||
self.assertEqual(issuer.text, generated_issuer)
|
||||
self.assertEqual(issuer.text, self.provider.issuer)
|
||||
|
||||
# Check Status
|
||||
status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")
|
||||
|
||||
@@ -85,6 +85,7 @@ class TestServiceProviderMetadataParser(TestCase):
|
||||
metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/simple.xml"))
|
||||
provider = metadata.to_provider("test", self.flow, self.flow)
|
||||
self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs")
|
||||
self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata")
|
||||
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
|
||||
self.assertEqual(provider.default_name_id_policy, SAMLNameIDPolicy.EMAIL)
|
||||
self.assertEqual(
|
||||
@@ -98,6 +99,7 @@ class TestServiceProviderMetadataParser(TestCase):
|
||||
metadata = ServiceProviderMetadataParser().parse(load_fixture("fixtures/cert.xml"))
|
||||
provider = metadata.to_provider("test", self.flow, self.flow)
|
||||
self.assertEqual(provider.acs_url, "http://localhost:8080/apps/user_saml/saml/acs")
|
||||
self.assertEqual(provider.issuer, "http://localhost:8080/apps/user_saml/saml/metadata")
|
||||
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
|
||||
self.assertEqual(
|
||||
provider.verification_kp.certificate_data, load_fixture("fixtures/cert.pem")
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestSAMLSessionModel(TestCase):
|
||||
name="test-provider",
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
)
|
||||
|
||||
# Create another provider for testing
|
||||
@@ -40,7 +40,7 @@ class TestSAMLSessionModel(TestCase):
|
||||
name="test-provider-2",
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp2.example.com/acs",
|
||||
issuer_override="https://idp2.example.com",
|
||||
issuer="https://idp2.example.com",
|
||||
)
|
||||
|
||||
# Create a session first (using authentik's custom Session model)
|
||||
@@ -72,7 +72,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Verify the session was created
|
||||
@@ -101,7 +100,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Try to create another session with same session_index and provider
|
||||
@@ -115,7 +113,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
def test_cascade_deletion_user(self):
|
||||
@@ -130,7 +127,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Verify session exists
|
||||
@@ -154,7 +150,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Verify session exists
|
||||
@@ -178,7 +173,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Verify session exists
|
||||
@@ -202,7 +196,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Create second session with different provider
|
||||
@@ -215,7 +208,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Verify both sessions exist
|
||||
@@ -237,7 +229,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=future_time,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Verify expiry time
|
||||
@@ -257,7 +248,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=past_time,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Check if marked as expired
|
||||
@@ -275,7 +265,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format="", # Blank format
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Verify it was created successfully
|
||||
@@ -294,7 +283,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
session2 = SAMLSession.objects.create(
|
||||
@@ -306,7 +294,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Query by provider
|
||||
@@ -329,7 +316,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Check serializer property
|
||||
@@ -348,7 +334,6 @@ class TestSAMLSessionModel(TestCase):
|
||||
name_id_format=self.name_id_format,
|
||||
expires=self.expires,
|
||||
expiring=True,
|
||||
issuer="authentik",
|
||||
)
|
||||
|
||||
# Verify sessions exist
|
||||
|
||||
@@ -7,7 +7,6 @@ from guardian.shortcuts import get_anonymous_user
|
||||
from lxml import etree # nosec
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import RequestFactory, create_test_cert, create_test_flow
|
||||
from authentik.lib.xml import lxml_from_string
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
@@ -31,11 +30,6 @@ class TestSchema(TestCase):
|
||||
)
|
||||
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
self.provider.save()
|
||||
Application.objects.create(
|
||||
name="test-app",
|
||||
slug="test-app",
|
||||
provider=self.provider,
|
||||
)
|
||||
self.source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestSendSamlLogoutResponse(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="https://sp.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
signing_kp=self.cert,
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestSendSamlLogoutRequest(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="https://sp.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
signing_kp=self.cert,
|
||||
)
|
||||
|
||||
@@ -155,7 +155,6 @@ class TestSendSamlLogoutRequest(TestCase):
|
||||
name_id="test@example.com",
|
||||
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
|
||||
session_index="test-session-123",
|
||||
issuer="https://idp.example.com",
|
||||
)
|
||||
|
||||
self.assertTrue(result)
|
||||
@@ -180,7 +179,6 @@ class TestSendSamlLogoutRequest(TestCase):
|
||||
name_id="test@example.com",
|
||||
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
|
||||
session_index="test-session-123",
|
||||
issuer="https://idp.example.com",
|
||||
)
|
||||
|
||||
self.assertFalse(result)
|
||||
@@ -200,7 +198,6 @@ class TestSendSamlLogoutRequest(TestCase):
|
||||
name_id="test@example.com",
|
||||
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
|
||||
session_index="test-session-123",
|
||||
issuer="https://idp.example.com",
|
||||
)
|
||||
|
||||
|
||||
@@ -217,7 +214,7 @@ class TestSendPostLogoutRequest(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="https://sp.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
signing_kp=self.cert,
|
||||
)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class TestSPInitiatedSLOViews(TestCase):
|
||||
invalidation_flow=self.invalidation_flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="https://sp.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="redirect",
|
||||
sls_binding="redirect",
|
||||
)
|
||||
@@ -90,7 +90,7 @@ class TestSPInitiatedSLOViews(TestCase):
|
||||
# Verify logout request was stored in plan context
|
||||
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
|
||||
logout_request = view.plan_context["authentik/providers/saml/logout_request"]
|
||||
self.assertEqual(logout_request.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(logout_request.issuer, self.provider.issuer)
|
||||
self.assertEqual(logout_request.session_index, "test-session-123")
|
||||
|
||||
def test_redirect_view_handles_logout_response_with_plan_context(self):
|
||||
@@ -228,7 +228,7 @@ class TestSPInitiatedSLOViews(TestCase):
|
||||
# Verify logout request was stored in plan context
|
||||
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
|
||||
logout_request = view.plan_context["authentik/providers/saml/logout_request"]
|
||||
self.assertEqual(logout_request.issuer, self.provider.issuer_override)
|
||||
self.assertEqual(logout_request.issuer, self.provider.issuer)
|
||||
self.assertEqual(logout_request.session_index, "test-session-123")
|
||||
|
||||
def test_post_view_handles_logout_response_with_plan_context(self):
|
||||
@@ -396,7 +396,7 @@ class TestSPInitiatedSLOViews(TestCase):
|
||||
authorization_flow=self.flow,
|
||||
acs_url="https://sp2.example.com/acs",
|
||||
sls_url="https://sp2.example.com/sls",
|
||||
issuer_override="https://idp2.example.com",
|
||||
issuer="https://idp2.example.com",
|
||||
invalidation_flow=None, # No invalidation flow
|
||||
)
|
||||
|
||||
@@ -524,7 +524,7 @@ class TestSPInitiatedSLOLogoutMethods(TestCase):
|
||||
invalidation_flow=self.invalidation_flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="https://sp.example.com/sls",
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
sp_binding="redirect",
|
||||
sls_binding="redirect",
|
||||
signing_kp=self.cert,
|
||||
@@ -714,7 +714,7 @@ class TestSPInitiatedSLOLogoutMethods(TestCase):
|
||||
invalidation_flow=self.invalidation_flow,
|
||||
acs_url="https://sp.example.com/acs",
|
||||
sls_url="", # No SLS URL
|
||||
issuer_override="https://idp.example.com",
|
||||
issuer="https://idp.example.com",
|
||||
)
|
||||
|
||||
app_no_sls = Application.objects.create(
|
||||
|
||||
@@ -11,12 +11,6 @@ from authentik.providers.saml.views.sp_slo import (
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# Base path for Issuer/Entity ID
|
||||
path(
|
||||
"<slug:application_slug>/",
|
||||
sso.SAMLSSOBindingRedirectView.as_view(),
|
||||
name="base",
|
||||
),
|
||||
# SSO Bindings
|
||||
path(
|
||||
"<slug:application_slug>/sso/binding/redirect/",
|
||||
|
||||
@@ -81,7 +81,6 @@ class SAMLFlowFinalView(ChallengeStageView):
|
||||
"session": auth_session,
|
||||
"name_id": processor.name_id,
|
||||
"name_id_format": processor.name_id_format,
|
||||
"issuer": processor.issuer,
|
||||
"expires": processor.session_not_on_or_after_datetime,
|
||||
"expiring": True,
|
||||
},
|
||||
|
||||
@@ -107,25 +107,12 @@ class SPInitiatedSLOView(PolicyAccessView):
|
||||
# Store relay state for the logout response
|
||||
plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
|
||||
|
||||
# Look up the session issuer to use in the logout response
|
||||
auth_session = AuthenticatedSession.from_request(request, request.user)
|
||||
session_issuer = None
|
||||
if auth_session:
|
||||
saml_session = SAMLSession.objects.filter(
|
||||
session=auth_session,
|
||||
user=request.user,
|
||||
provider=self.provider,
|
||||
).first()
|
||||
if saml_session:
|
||||
session_issuer = saml_session.issuer
|
||||
|
||||
if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
|
||||
# Native mode - user will be redirected/posted away from authentik
|
||||
processor = LogoutResponseProcessor(
|
||||
self.provider,
|
||||
logout_request,
|
||||
destination=self.provider.sls_url,
|
||||
issuer=session_issuer,
|
||||
)
|
||||
|
||||
if self.provider.sls_binding == SAMLBindings.POST:
|
||||
@@ -165,7 +152,6 @@ class SPInitiatedSLOView(PolicyAccessView):
|
||||
sls_url=self.provider.sls_url,
|
||||
logout_request_id=logout_request.id if logout_request else None,
|
||||
relay_state=relay_state,
|
||||
issuer=session_issuer,
|
||||
)
|
||||
|
||||
LOGGER.debug(
|
||||
@@ -182,7 +168,6 @@ class SPInitiatedSLOView(PolicyAccessView):
|
||||
self.provider,
|
||||
logout_request,
|
||||
destination=self.provider.sls_url,
|
||||
issuer=session_issuer,
|
||||
)
|
||||
|
||||
logout_response = processor.build_response()
|
||||
|
||||
@@ -5,6 +5,7 @@ from hashlib import sha512
|
||||
from ipaddress import ip_address
|
||||
from time import perf_counter, time
|
||||
from typing import Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from channels.exceptions import DenyConnection
|
||||
from django.conf import settings
|
||||
@@ -314,6 +315,126 @@ class ChannelsLoggingMiddleware:
|
||||
)
|
||||
|
||||
|
||||
CSP_HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"
|
||||
CSP_HEADER_ENFORCE = "Content-Security-Policy"
|
||||
|
||||
|
||||
class ContentSecurityPolicyMiddleware:
|
||||
"""Emit a Content-Security-Policy(-Report-Only) header carrying the per-request nonce.
|
||||
|
||||
The policy is intentionally strict: inline `<script>`/`<style>` are rejected unless
|
||||
they carry the request's nonce (set via `request.request_id` and exposed to templates
|
||||
as `csp_nonce`). External resources from third-party login providers (Apple, Telegram)
|
||||
and configurable captcha hosts are allow-listed below; the report-only mode lets the
|
||||
browser surface anything else as a console violation without breaking the page.
|
||||
"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
||||
# Hosts that the bundled login flows pull resources from. The captcha stage allows
|
||||
# an admin-configured `js_url`, so the well-known third-party captcha origins are
|
||||
# included here so that report-only output is not drowned in expected violations.
|
||||
SCRIPT_SRC_THIRD_PARTY = (
|
||||
"https://appleid.cdn-apple.com",
|
||||
"https://telegram.org",
|
||||
"https://www.google.com",
|
||||
"https://www.gstatic.com",
|
||||
"https://www.recaptcha.net",
|
||||
"https://js.hcaptcha.com",
|
||||
"https://challenges.cloudflare.com",
|
||||
)
|
||||
FRAME_SRC_THIRD_PARTY = (
|
||||
"https://appleid.apple.com",
|
||||
"https://oauth.telegram.org",
|
||||
"https://www.google.com",
|
||||
"https://newassets.hcaptcha.com",
|
||||
"https://challenges.cloudflare.com",
|
||||
)
|
||||
|
||||
# Dev-only origins. The esbuild live-reload plugin opens an EventSource against
|
||||
# a dynamically chosen localhost port, so localhost on any scheme/port is allowed
|
||||
# when DEBUG is on. Never folded into prod policy.
|
||||
DEBUG_CONNECT_SRC = (
|
||||
"http://localhost:*",
|
||||
"https://localhost:*",
|
||||
"ws://localhost:*",
|
||||
"wss://localhost:*",
|
||||
)
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
self.report_only = CONFIG.get_bool("web.csp.report_only", True)
|
||||
self.debug = settings.DEBUG
|
||||
self.sentry_origin = self._sentry_origin(CONFIG.get("error_reporting.sentry_dsn", ""))
|
||||
|
||||
@staticmethod
|
||||
def _sentry_origin(dsn: str) -> str | None:
|
||||
"""Pull `scheme://host[:port]` out of a Sentry DSN so the browser SDK
|
||||
can ship envelopes to it. DSNs are `https://<key>@<host>/<project>`."""
|
||||
if not dsn:
|
||||
return None
|
||||
parts = urlsplit(dsn)
|
||||
if not parts.scheme or not parts.hostname:
|
||||
return None
|
||||
host = parts.hostname
|
||||
if parts.port:
|
||||
host = f"{host}:{parts.port}"
|
||||
return f"{parts.scheme}://{host}"
|
||||
|
||||
def _build_policy(self, nonce: str) -> str:
|
||||
nonce_token = f"'nonce-{nonce}'"
|
||||
script_src = ("'self'", nonce_token, *self.SCRIPT_SRC_THIRD_PARTY)
|
||||
# Per CSP3 §6.6.2.2, browsers ignore `'unsafe-inline'` whenever a
|
||||
# nonce is also present in the same source list. Several runtime
|
||||
# libraries we ship (mermaid, PatternFly's own style injections,
|
||||
# DOMPurify's sanitization sandbox) emit `<style>` elements
|
||||
# dynamically without a nonce, so we drop the nonce for styles
|
||||
# and rely on `'unsafe-inline'`. Script-side CSP is unaffected
|
||||
# — the eval/script protections remain strict.
|
||||
style_src = ("'self'", "'unsafe-inline'")
|
||||
frame_src = ("'self'", *self.FRAME_SRC_THIRD_PARTY)
|
||||
connect_src: tuple[str, ...] = ("'self'", "ws:", "wss:")
|
||||
if self.sentry_origin:
|
||||
connect_src = (*connect_src, self.sentry_origin)
|
||||
if self.debug:
|
||||
connect_src = (*connect_src, *self.DEBUG_CONNECT_SRC)
|
||||
directives = {
|
||||
"default-src": ("'self'",),
|
||||
"script-src": script_src,
|
||||
"style-src": style_src,
|
||||
# Inline `style="..."` attributes can't carry a nonce; many libraries
|
||||
# (PatternFly, Lit style bindings) set them dynamically.
|
||||
"style-src-attr": ("'unsafe-inline'",),
|
||||
"img-src": ("'self'", "data:", "blob:", "https:"),
|
||||
"font-src": ("'self'", "data:"),
|
||||
"connect-src": connect_src,
|
||||
"frame-src": frame_src,
|
||||
"media-src": ("'self'",),
|
||||
"worker-src": ("'self'", "blob:"),
|
||||
"object-src": ("'none'",),
|
||||
"base-uri": ("'self'",),
|
||||
"form-action": ("'self'",),
|
||||
"frame-ancestors": ("'none'",),
|
||||
}
|
||||
return "; ".join(f"{name} {' '.join(values)}" for name, values in directives.items())
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
response = self.get_response(request)
|
||||
# Only attach to HTML responses — CSP on JSON/binary responses is just header bloat.
|
||||
content_type = response.get("Content-Type", "")
|
||||
if not content_type.startswith("text/html"):
|
||||
return response
|
||||
nonce = getattr(request, "request_id", None)
|
||||
if not nonce:
|
||||
return response
|
||||
header = CSP_HEADER_REPORT_ONLY if self.report_only else CSP_HEADER_ENFORCE
|
||||
# Don't clobber a policy a downstream view explicitly set.
|
||||
if header in response:
|
||||
return response
|
||||
response[header] = self._build_policy(nonce)
|
||||
return response
|
||||
|
||||
|
||||
class LoggingMiddleware:
|
||||
"""Logger middleware"""
|
||||
|
||||
|
||||
@@ -276,6 +276,7 @@ MIDDLEWARE = [
|
||||
"authentik.core.middleware.RequestIDMiddleware",
|
||||
"authentik.brands.middleware.BrandMiddleware",
|
||||
"authentik.events.middleware.AuditMiddleware",
|
||||
"authentik.root.middleware.ContentSecurityPolicyMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"authentik.root.middleware.CsrfViewMiddleware",
|
||||
|
||||
@@ -94,7 +94,7 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
def get_user_id(self, info: dict[str, Any]) -> str | None:
|
||||
"""Return unique identifier from the profile info."""
|
||||
if "id" in info:
|
||||
return str(info["id"])
|
||||
return info["id"]
|
||||
return None
|
||||
|
||||
def handle_login_failure(self, reason: str) -> HttpResponse:
|
||||
|
||||
@@ -353,7 +353,7 @@ class IdentificationStageView(ChallengeStageView):
|
||||
PLAN_CONTEXT_APPLICATION, Application()
|
||||
)
|
||||
challenge.initial_data["application_pre"] = app.name
|
||||
if not app.meta_hide and (launch_url := app.get_launch_url()):
|
||||
if launch_url := app.get_launch_url():
|
||||
challenge.initial_data["application_pre_launch"] = launch_url
|
||||
if (
|
||||
PLAN_CONTEXT_DEVICE in self.executor.plan.context
|
||||
|
||||
@@ -5215,11 +5215,6 @@
|
||||
"type": "string",
|
||||
"title": "Group"
|
||||
},
|
||||
"meta_hide": {
|
||||
"type": "boolean",
|
||||
"title": "Meta hide",
|
||||
"description": "Hide this application from the user's My applications page."
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@@ -5537,14 +5532,6 @@
|
||||
"minLength": 1,
|
||||
"title": "Password"
|
||||
},
|
||||
"password_hash": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"minLength": 1,
|
||||
"title": "Password hash"
|
||||
},
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -10825,10 +10812,11 @@
|
||||
"title": "Audience",
|
||||
"description": "Value of the audience restriction field of the assertion. When left empty, no audience restriction will be added."
|
||||
},
|
||||
"issuer_override": {
|
||||
"issuer": {
|
||||
"type": "string",
|
||||
"title": "Issuer override",
|
||||
"description": "Also known as EntityID. Providing a value overrides the default issuer generated by authentik."
|
||||
"minLength": 1,
|
||||
"title": "Issuer",
|
||||
"description": "Also known as EntityID"
|
||||
},
|
||||
"assertion_valid_not_before": {
|
||||
"type": "string",
|
||||
|
||||
@@ -11,7 +11,6 @@ context:
|
||||
group_name: authentik Admins
|
||||
email: !Env [AUTHENTIK_BOOTSTRAP_EMAIL, "root@example.com"]
|
||||
password: !Env [AUTHENTIK_BOOTSTRAP_PASSWORD, null]
|
||||
password_hash: !Env [AUTHENTIK_BOOTSTRAP_PASSWORD_HASH, null]
|
||||
token: !Env [AUTHENTIK_BOOTSTRAP_TOKEN, null]
|
||||
entries:
|
||||
- model: authentik_core.group
|
||||
@@ -32,7 +31,6 @@ entries:
|
||||
groups:
|
||||
- !KeyOf admin-group
|
||||
password: !Context password
|
||||
password_hash: !Context password_hash
|
||||
- model: authentik_core.token
|
||||
state: created
|
||||
conditions:
|
||||
|
||||
6
build.rs
6
build.rs
@@ -1,6 +0,0 @@
|
||||
fn main() {
|
||||
#[cfg(feature = "core")]
|
||||
{
|
||||
pyo3_build_config::add_libpython_rpath_link_args();
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -7,7 +7,7 @@ require (
|
||||
beryju.io/radius-eap v0.1.0
|
||||
github.com/avast/retry-go/v4 v4.7.0
|
||||
github.com/coreos/go-oidc/v3 v3.18.0
|
||||
github.com/getsentry/sentry-go v0.46.0
|
||||
github.com/getsentry/sentry-go v0.45.1
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/go-openapi/runtime v0.29.4
|
||||
|
||||
4
go.sum
4
go.sum
@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/getsentry/sentry-go v0.46.0 h1:mbdDaarbUdOt9X+dx6kDdntkShLEX3/+KyOsVDTPDj0=
|
||||
github.com/getsentry/sentry-go v0.46.0/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
|
||||
github.com/getsentry/sentry-go v0.45.1 h1:9rfzJtGiJG+MGIaWZXidDGHcH5GU1Z5y0WVJGf9nysw=
|
||||
github.com/getsentry/sentry-go v0.45.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Wrapper for lifecycle/ak, to be installed by uv"""
|
||||
|
||||
import subprocess
|
||||
from os import system, waitstatus_to_exitcode
|
||||
from pathlib import Path
|
||||
from sys import argv, exit
|
||||
|
||||
|
||||
def main():
|
||||
"""Wrapper around ak bash script"""
|
||||
script = Path(__file__).parent / "ak"
|
||||
res = subprocess.run([str(script), *argv[1:]], check=False)
|
||||
exit(res.returncode)
|
||||
current_path = Path(__file__)
|
||||
args = " ".join(argv[1:])
|
||||
res = system(f"{current_path.parent}/ak {args}") # nosec
|
||||
exit_code = waitstatus_to_exitcode(res)
|
||||
exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -147,7 +147,6 @@ RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
|
||||
--mount=type=bind,target=Cargo.toml,src=Cargo.toml \
|
||||
--mount=type=bind,target=Cargo.lock,src=Cargo.lock \
|
||||
--mount=type=bind,target=.cargo/,src=.cargo/ \
|
||||
--mount=type=bind,target=build.rs,src=build.rs \
|
||||
--mount=type=bind,target=src/,src=src/ \
|
||||
--mount=type=bind,target=packages/ak-axum,src=packages/ak-axum \
|
||||
--mount=type=bind,target=packages/ak-common,src=packages/ak-common \
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-29 00:28+0000\n"
|
||||
"POT-Creation-Date: 2026-04-23 00:25+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -392,10 +392,6 @@ msgstr ""
|
||||
msgid "Open launch URL in a new browser tab or window."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Hide this application from the user's My applications page."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Application"
|
||||
msgstr ""
|
||||
@@ -2495,9 +2491,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Also known as EntityID. Providing a value overrides the default issuer "
|
||||
"generated by authentik."
|
||||
msgid "Also known as EntityID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
@@ -2691,10 +2685,6 @@ msgstr ""
|
||||
msgid "SAML NameID format"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SAML Issuer used for this session"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SAML Session"
|
||||
msgstr ""
|
||||
@@ -2727,10 +2717,6 @@ msgstr ""
|
||||
msgid "Webex"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "vCenter"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Group filters used to define sync-scope for groups."
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,7 @@
|
||||
//! Utilities for working with [`Router`].
|
||||
|
||||
use ak_common::config;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::Request,
|
||||
http::{HeaderName, HeaderValue, StatusCode},
|
||||
middleware::{Next, from_fn},
|
||||
response::Response,
|
||||
};
|
||||
use axum::{Router, http::StatusCode, middleware::from_fn};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::timeout::TimeoutLayer;
|
||||
|
||||
@@ -19,16 +13,6 @@ use crate::{
|
||||
tracing::{span_middleware, tracing_middleware},
|
||||
};
|
||||
|
||||
const X_POWERED_BY: HeaderName = HeaderName::from_static("x-powered-by");
|
||||
|
||||
async fn powered_by_authentik_middleware(request: Request, next: Next) -> Response {
|
||||
let mut response = next.run(request).await;
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(X_POWERED_BY, HeaderValue::from_static("authentik"));
|
||||
response
|
||||
}
|
||||
|
||||
/// Wrap a [`Router`] with common middlewares.
|
||||
///
|
||||
/// Set `with_tracing` to [`true`] to log requests.
|
||||
@@ -46,7 +30,6 @@ pub fn wrap_router(router: Router, with_tracing: bool) -> Router {
|
||||
timeout,
|
||||
))
|
||||
.layer(from_fn(span_middleware))
|
||||
.layer(from_fn(powered_by_authentik_middleware))
|
||||
.layer(from_fn(trusted_proxy_middleware))
|
||||
.layer(from_fn(client_ip_middleware))
|
||||
.layer(from_fn(scheme_middleware))
|
||||
|
||||
78
packages/client-ts/src/apis/CoreApi.ts
generated
78
packages/client-ts/src/apis/CoreApi.ts
generated
@@ -54,7 +54,6 @@ import type {
|
||||
User,
|
||||
UserAccountRequest,
|
||||
UserConsent,
|
||||
UserPasswordHashSetRequest,
|
||||
UserPasswordSetRequest,
|
||||
UserPath,
|
||||
UserRecoveryEmailRequest,
|
||||
@@ -105,7 +104,6 @@ import {
|
||||
UserAccountRequestToJSON,
|
||||
UserConsentFromJSON,
|
||||
UserFromJSON,
|
||||
UserPasswordHashSetRequestToJSON,
|
||||
UserPasswordSetRequestToJSON,
|
||||
UserPathFromJSON,
|
||||
UserRecoveryEmailRequestToJSON,
|
||||
@@ -510,11 +508,6 @@ export interface CoreUsersSetPasswordCreateRequest {
|
||||
userPasswordSetRequest: UserPasswordSetRequest;
|
||||
}
|
||||
|
||||
export interface CoreUsersSetPasswordHashCreateRequest {
|
||||
id: number;
|
||||
userPasswordHashSetRequest: UserPasswordHashSetRequest;
|
||||
}
|
||||
|
||||
export interface CoreUsersUpdateRequest {
|
||||
id: number;
|
||||
userRequest: UserRequest;
|
||||
@@ -5295,77 +5288,6 @@ export class CoreApi extends runtime.BaseAPI {
|
||||
await this.coreUsersSetPasswordCreateRaw(requestParameters, initOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreUsersSetPasswordHashCreate without sending the request
|
||||
*/
|
||||
async coreUsersSetPasswordHashCreateRequestOpts(
|
||||
requestParameters: CoreUsersSetPasswordHashCreateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["id"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"id",
|
||||
'Required parameter "id" was null or undefined when calling coreUsersSetPasswordHashCreate().',
|
||||
);
|
||||
}
|
||||
|
||||
if (requestParameters["userPasswordHashSetRequest"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"userPasswordHashSetRequest",
|
||||
'Required parameter "userPasswordHashSetRequest" was null or undefined when calling coreUsersSetPasswordHashCreate().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters["Content-Type"] = "application/json";
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/core/users/{id}/set_password_hash/`;
|
||||
urlPath = urlPath.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters["id"])));
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "POST",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: UserPasswordHashSetRequestToJSON(requestParameters["userPasswordHashSetRequest"]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user\'s password from a pre-hashed Django password value. Submit the Django password hash in the shared ``password`` request field. This updates authentik\'s local password verifier only. It does not attempt to propagate the password change to LDAP or Kerberos because no raw password is available from the request payload.
|
||||
*/
|
||||
async coreUsersSetPasswordHashCreateRaw(
|
||||
requestParameters: CoreUsersSetPasswordHashCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<void>> {
|
||||
const requestOptions =
|
||||
await this.coreUsersSetPasswordHashCreateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.VoidApiResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user\'s password from a pre-hashed Django password value. Submit the Django password hash in the shared ``password`` request field. This updates authentik\'s local password verifier only. It does not attempt to propagate the password change to LDAP or Kerberos because no raw password is available from the request payload.
|
||||
*/
|
||||
async coreUsersSetPasswordHashCreate(
|
||||
requestParameters: CoreUsersSetPasswordHashCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<void> {
|
||||
await this.coreUsersSetPasswordHashCreateRaw(requestParameters, initOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreUsersUpdate without sending the request
|
||||
*/
|
||||
|
||||
12
packages/client-ts/src/apis/ProvidersApi.ts
generated
12
packages/client-ts/src/apis/ProvidersApi.ts
generated
@@ -634,7 +634,7 @@ export interface ProvidersSamlListRequest {
|
||||
encryptionKp?: string;
|
||||
invalidationFlow?: string;
|
||||
isBackchannel?: boolean;
|
||||
issuerOverride?: string;
|
||||
issuer?: string;
|
||||
logoutMethod?: SAMLLogoutMethods;
|
||||
name?: string;
|
||||
nameIdMapping?: string;
|
||||
@@ -841,7 +841,7 @@ export interface ProvidersWsfedListRequest {
|
||||
encryptionKp?: string;
|
||||
invalidationFlow?: string;
|
||||
isBackchannel?: boolean;
|
||||
issuerOverride?: string;
|
||||
issuer?: string;
|
||||
logoutMethod?: SAMLLogoutMethods;
|
||||
name?: string;
|
||||
nameIdMapping?: string;
|
||||
@@ -6842,8 +6842,8 @@ export class ProvidersApi extends runtime.BaseAPI {
|
||||
queryParameters["is_backchannel"] = requestParameters["isBackchannel"];
|
||||
}
|
||||
|
||||
if (requestParameters["issuerOverride"] != null) {
|
||||
queryParameters["issuer_override"] = requestParameters["issuerOverride"];
|
||||
if (requestParameters["issuer"] != null) {
|
||||
queryParameters["issuer"] = requestParameters["issuer"];
|
||||
}
|
||||
|
||||
if (requestParameters["logoutMethod"] != null) {
|
||||
@@ -9326,8 +9326,8 @@ export class ProvidersApi extends runtime.BaseAPI {
|
||||
queryParameters["is_backchannel"] = requestParameters["isBackchannel"];
|
||||
}
|
||||
|
||||
if (requestParameters["issuerOverride"] != null) {
|
||||
queryParameters["issuer_override"] = requestParameters["issuerOverride"];
|
||||
if (requestParameters["issuer"] != null) {
|
||||
queryParameters["issuer"] = requestParameters["issuer"];
|
||||
}
|
||||
|
||||
if (requestParameters["logoutMethod"] != null) {
|
||||
|
||||
8
packages/client-ts/src/models/Application.ts
generated
8
packages/client-ts/src/models/Application.ts
generated
@@ -127,12 +127,6 @@ export interface Application {
|
||||
* @memberof Application
|
||||
*/
|
||||
group?: string;
|
||||
/**
|
||||
* Hide this application from the user's My applications page.
|
||||
* @type {boolean}
|
||||
* @memberof Application
|
||||
*/
|
||||
metaHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,7 +177,6 @@ export function ApplicationFromJSONTyped(json: any, ignoreDiscriminator: boolean
|
||||
? undefined
|
||||
: PolicyEngineModeFromJSON(json["policy_engine_mode"]),
|
||||
group: json["group"] == null ? undefined : json["group"],
|
||||
metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,6 +212,5 @@ export function ApplicationToJSONTyped(
|
||||
meta_publisher: value["metaPublisher"],
|
||||
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
|
||||
group: value["group"],
|
||||
meta_hide: value["metaHide"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,12 +87,6 @@ export interface ApplicationRequest {
|
||||
* @memberof ApplicationRequest
|
||||
*/
|
||||
group?: string;
|
||||
/**
|
||||
* Hide this application from the user's My applications page.
|
||||
* @type {boolean}
|
||||
* @memberof ApplicationRequest
|
||||
*/
|
||||
metaHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +125,6 @@ export function ApplicationRequestFromJSONTyped(
|
||||
? undefined
|
||||
: PolicyEngineModeFromJSON(json["policy_engine_mode"]),
|
||||
group: json["group"] == null ? undefined : json["group"],
|
||||
metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,6 +152,5 @@ export function ApplicationRequestToJSONTyped(
|
||||
meta_publisher: value["metaPublisher"],
|
||||
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
|
||||
group: value["group"],
|
||||
meta_hide: value["metaHide"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,12 +87,6 @@ export interface PatchedApplicationRequest {
|
||||
* @memberof PatchedApplicationRequest
|
||||
*/
|
||||
group?: string;
|
||||
/**
|
||||
* Hide this application from the user's My applications page.
|
||||
* @type {boolean}
|
||||
* @memberof PatchedApplicationRequest
|
||||
*/
|
||||
metaHide?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +125,6 @@ export function PatchedApplicationRequestFromJSONTyped(
|
||||
? undefined
|
||||
: PolicyEngineModeFromJSON(json["policy_engine_mode"]),
|
||||
group: json["group"] == null ? undefined : json["group"],
|
||||
metaHide: json["meta_hide"] == null ? undefined : json["meta_hide"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,6 +152,5 @@ export function PatchedApplicationRequestToJSONTyped(
|
||||
meta_publisher: value["metaPublisher"],
|
||||
policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]),
|
||||
group: value["group"],
|
||||
meta_hide: value["metaHide"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,11 +81,11 @@ export interface PatchedSAMLProviderRequest {
|
||||
*/
|
||||
audience?: string;
|
||||
/**
|
||||
* Also known as EntityID. Providing a value overrides the default issuer generated by authentik.
|
||||
* Also known as EntityID
|
||||
* @type {string}
|
||||
* @memberof PatchedSAMLProviderRequest
|
||||
*/
|
||||
issuerOverride?: string;
|
||||
issuer?: string;
|
||||
/**
|
||||
* Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3).
|
||||
* @type {string}
|
||||
@@ -233,7 +233,7 @@ export function PatchedSAMLProviderRequestFromJSONTyped(
|
||||
acsUrl: json["acs_url"] == null ? undefined : json["acs_url"],
|
||||
slsUrl: json["sls_url"] == null ? undefined : json["sls_url"],
|
||||
audience: json["audience"] == null ? undefined : json["audience"],
|
||||
issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"],
|
||||
issuer: json["issuer"] == null ? undefined : json["issuer"],
|
||||
assertionValidNotBefore:
|
||||
json["assertion_valid_not_before"] == null
|
||||
? undefined
|
||||
@@ -306,7 +306,7 @@ export function PatchedSAMLProviderRequestToJSONTyped(
|
||||
acs_url: value["acsUrl"],
|
||||
sls_url: value["slsUrl"],
|
||||
audience: value["audience"],
|
||||
issuer_override: value["issuerOverride"],
|
||||
issuer: value["issuer"],
|
||||
assertion_valid_not_before: value["assertionValidNotBefore"],
|
||||
assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"],
|
||||
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],
|
||||
|
||||
17
packages/client-ts/src/models/SAMLProvider.ts
generated
17
packages/client-ts/src/models/SAMLProvider.ts
generated
@@ -135,11 +135,11 @@ export interface SAMLProvider {
|
||||
*/
|
||||
audience?: string;
|
||||
/**
|
||||
* Also known as EntityID. Providing a value overrides the default issuer generated by authentik.
|
||||
* Also known as EntityID
|
||||
* @type {string}
|
||||
* @memberof SAMLProvider
|
||||
*/
|
||||
issuerOverride?: string;
|
||||
issuer?: string;
|
||||
/**
|
||||
* Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3).
|
||||
* @type {string}
|
||||
@@ -260,12 +260,6 @@ export interface SAMLProvider {
|
||||
* @memberof SAMLProvider
|
||||
*/
|
||||
readonly urlDownloadMetadata: string;
|
||||
/**
|
||||
* Get Issuer/EntityID URL
|
||||
* @type {string}
|
||||
* @memberof SAMLProvider
|
||||
*/
|
||||
readonly urlIssuer: string;
|
||||
/**
|
||||
* Get SSO Post URL
|
||||
* @type {string}
|
||||
@@ -327,7 +321,6 @@ export function instanceOfSAMLProvider(value: object): value is SAMLProvider {
|
||||
if (!("acsUrl" in value) || value["acsUrl"] === undefined) return false;
|
||||
if (!("urlDownloadMetadata" in value) || value["urlDownloadMetadata"] === undefined)
|
||||
return false;
|
||||
if (!("urlIssuer" in value) || value["urlIssuer"] === undefined) return false;
|
||||
if (!("urlSsoPost" in value) || value["urlSsoPost"] === undefined) return false;
|
||||
if (!("urlSsoRedirect" in value) || value["urlSsoRedirect"] === undefined) return false;
|
||||
if (!("urlSsoInit" in value) || value["urlSsoInit"] === undefined) return false;
|
||||
@@ -363,7 +356,7 @@ export function SAMLProviderFromJSONTyped(json: any, ignoreDiscriminator: boolea
|
||||
acsUrl: json["acs_url"],
|
||||
slsUrl: json["sls_url"] == null ? undefined : json["sls_url"],
|
||||
audience: json["audience"] == null ? undefined : json["audience"],
|
||||
issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"],
|
||||
issuer: json["issuer"] == null ? undefined : json["issuer"],
|
||||
assertionValidNotBefore:
|
||||
json["assertion_valid_not_before"] == null
|
||||
? undefined
|
||||
@@ -413,7 +406,6 @@ export function SAMLProviderFromJSONTyped(json: any, ignoreDiscriminator: boolea
|
||||
? undefined
|
||||
: SAMLNameIDPolicyEnumFromJSON(json["default_name_id_policy"]),
|
||||
urlDownloadMetadata: json["url_download_metadata"],
|
||||
urlIssuer: json["url_issuer"],
|
||||
urlSsoPost: json["url_sso_post"],
|
||||
urlSsoRedirect: json["url_sso_redirect"],
|
||||
urlSsoInit: json["url_sso_init"],
|
||||
@@ -439,7 +431,6 @@ export function SAMLProviderToJSONTyped(
|
||||
| "verbose_name_plural"
|
||||
| "meta_model_name"
|
||||
| "url_download_metadata"
|
||||
| "url_issuer"
|
||||
| "url_sso_post"
|
||||
| "url_sso_redirect"
|
||||
| "url_sso_init"
|
||||
@@ -461,7 +452,7 @@ export function SAMLProviderToJSONTyped(
|
||||
acs_url: value["acsUrl"],
|
||||
sls_url: value["slsUrl"],
|
||||
audience: value["audience"],
|
||||
issuer_override: value["issuerOverride"],
|
||||
issuer: value["issuer"],
|
||||
assertion_valid_not_before: value["assertionValidNotBefore"],
|
||||
assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"],
|
||||
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],
|
||||
|
||||
@@ -81,11 +81,11 @@ export interface SAMLProviderRequest {
|
||||
*/
|
||||
audience?: string;
|
||||
/**
|
||||
* Also known as EntityID. Providing a value overrides the default issuer generated by authentik.
|
||||
* Also known as EntityID
|
||||
* @type {string}
|
||||
* @memberof SAMLProviderRequest
|
||||
*/
|
||||
issuerOverride?: string;
|
||||
issuer?: string;
|
||||
/**
|
||||
* Assertion valid not before current time + this value (Format: hours=-1;minutes=-2;seconds=-3).
|
||||
* @type {string}
|
||||
@@ -234,7 +234,7 @@ export function SAMLProviderRequestFromJSONTyped(
|
||||
acsUrl: json["acs_url"],
|
||||
slsUrl: json["sls_url"] == null ? undefined : json["sls_url"],
|
||||
audience: json["audience"] == null ? undefined : json["audience"],
|
||||
issuerOverride: json["issuer_override"] == null ? undefined : json["issuer_override"],
|
||||
issuer: json["issuer"] == null ? undefined : json["issuer"],
|
||||
assertionValidNotBefore:
|
||||
json["assertion_valid_not_before"] == null
|
||||
? undefined
|
||||
@@ -307,7 +307,7 @@ export function SAMLProviderRequestToJSONTyped(
|
||||
acs_url: value["acsUrl"],
|
||||
sls_url: value["slsUrl"],
|
||||
audience: value["audience"],
|
||||
issuer_override: value["issuerOverride"],
|
||||
issuer: value["issuer"],
|
||||
assertion_valid_not_before: value["assertionValidNotBefore"],
|
||||
assertion_valid_not_on_or_after: value["assertionValidNotOnOrAfter"],
|
||||
session_valid_not_on_or_after: value["sessionValidNotOnOrAfter"],
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Payload to set a users' password hash directly
|
||||
* @export
|
||||
* @interface UserPasswordHashSetRequest
|
||||
*/
|
||||
export interface UserPasswordHashSetRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserPasswordHashSetRequest
|
||||
*/
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the UserPasswordHashSetRequest interface.
|
||||
*/
|
||||
export function instanceOfUserPasswordHashSetRequest(
|
||||
value: object,
|
||||
): value is UserPasswordHashSetRequest {
|
||||
if (!("password" in value) || value["password"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function UserPasswordHashSetRequestFromJSON(json: any): UserPasswordHashSetRequest {
|
||||
return UserPasswordHashSetRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserPasswordHashSetRequestFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): UserPasswordHashSetRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
password: json["password"],
|
||||
};
|
||||
}
|
||||
|
||||
export function UserPasswordHashSetRequestToJSON(json: any): UserPasswordHashSetRequest {
|
||||
return UserPasswordHashSetRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserPasswordHashSetRequestToJSONTyped(
|
||||
value?: UserPasswordHashSetRequest | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
password: value["password"],
|
||||
};
|
||||
}
|
||||
1
packages/client-ts/src/models/index.ts
generated
1
packages/client-ts/src/models/index.ts
generated
@@ -842,7 +842,6 @@ export * from "./UserLogoutStageRequest";
|
||||
export * from "./UserMatchingModeEnum";
|
||||
export * from "./UserOAuthSourceConnection";
|
||||
export * from "./UserOAuthSourceConnectionRequest";
|
||||
export * from "./UserPasswordHashSetRequest";
|
||||
export * from "./UserPasswordSetRequest";
|
||||
export * from "./UserPath";
|
||||
export * from "./UserPlexSourceConnection";
|
||||
|
||||
@@ -66,7 +66,7 @@ dependencies = [
|
||||
"ua-parser==1.0.2",
|
||||
"unidecode==1.4.0",
|
||||
"urllib3<3",
|
||||
"uvicorn[standard]==0.45.0",
|
||||
"uvicorn[standard]==0.44.0",
|
||||
"watchdog==6.0.0",
|
||||
"webauthn==2.7.1",
|
||||
"wsproto==1.3.2",
|
||||
@@ -97,13 +97,13 @@ dev = [
|
||||
"pytest-django==4.12.0",
|
||||
"pytest-flakefinder==1.1.0",
|
||||
"pytest-github-actions-annotate-failures==0.4.0",
|
||||
"pytest-randomly==4.1.0",
|
||||
"pytest-randomly==4.0.1",
|
||||
"pytest-timeout==2.4.0",
|
||||
"pytest==9.0.3",
|
||||
"requests-mock==1.12.1",
|
||||
"ruff==0.15.12",
|
||||
"ruff==0.15.11",
|
||||
"selenium==4.43.0",
|
||||
"types-channels==4.3.0.20260421",
|
||||
"types-channels==4.3.0.20260408",
|
||||
"types-docker==7.1.0.20260409",
|
||||
"types-jwcrypto==1.5.7.20260409",
|
||||
"types-ldap3==2.9.13.20260408",
|
||||
|
||||
79
schema.yml
79
schema.yml
@@ -4522,41 +4522,6 @@ paths:
|
||||
description: Bad request
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/users/{id}/set_password_hash/:
|
||||
post:
|
||||
operationId: core_users_set_password_hash_create
|
||||
description: |-
|
||||
Set a user's password from a pre-hashed Django password value.
|
||||
|
||||
Submit the Django password hash in the shared ``password`` request field.
|
||||
|
||||
This updates authentik's local password verifier only. It does not attempt
|
||||
to propagate the password change to LDAP or Kerberos because no raw password
|
||||
is available from the request payload.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserPasswordHashSetRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: Successfully changed password
|
||||
'400':
|
||||
description: Bad request
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/users/{id}/used_by/:
|
||||
get:
|
||||
operationId: core_users_used_by_list
|
||||
@@ -18954,7 +18919,7 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: issuer_override
|
||||
name: issuer
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
@@ -20113,7 +20078,7 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: issuer_override
|
||||
name: issuer
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
@@ -34146,9 +34111,6 @@ components:
|
||||
$ref: '#/components/schemas/PolicyEngineMode'
|
||||
group:
|
||||
type: string
|
||||
meta_hide:
|
||||
type: boolean
|
||||
description: Hide this application from the user's My applications page.
|
||||
required:
|
||||
- backchannel_providers_obj
|
||||
- launch_url
|
||||
@@ -34230,9 +34192,6 @@ components:
|
||||
$ref: '#/components/schemas/PolicyEngineMode'
|
||||
group:
|
||||
type: string
|
||||
meta_hide:
|
||||
type: boolean
|
||||
description: Hide this application from the user's My applications page.
|
||||
required:
|
||||
- name
|
||||
- slug
|
||||
@@ -47469,9 +47428,6 @@ components:
|
||||
$ref: '#/components/schemas/PolicyEngineMode'
|
||||
group:
|
||||
type: string
|
||||
meta_hide:
|
||||
type: boolean
|
||||
description: Hide this application from the user's My applications page.
|
||||
PatchedAuthenticatorDuoStageRequest:
|
||||
type: object
|
||||
description: AuthenticatorDuoStage Serializer
|
||||
@@ -50237,10 +50193,10 @@ components:
|
||||
type: string
|
||||
description: Value of the audience restriction field of the assertion. When
|
||||
left empty, no audience restriction will be added.
|
||||
issuer_override:
|
||||
issuer:
|
||||
type: string
|
||||
description: Also known as EntityID. Providing a value overrides the default
|
||||
issuer generated by authentik.
|
||||
minLength: 1
|
||||
description: Also known as EntityID
|
||||
assertion_valid_not_before:
|
||||
type: string
|
||||
minLength: 1
|
||||
@@ -53759,10 +53715,9 @@ components:
|
||||
type: string
|
||||
description: Value of the audience restriction field of the assertion. When
|
||||
left empty, no audience restriction will be added.
|
||||
issuer_override:
|
||||
issuer:
|
||||
type: string
|
||||
description: Also known as EntityID. Providing a value overrides the default
|
||||
issuer generated by authentik.
|
||||
description: Also known as EntityID
|
||||
assertion_valid_not_before:
|
||||
type: string
|
||||
description: 'Assertion valid not before current time + this value (Format:
|
||||
@@ -53852,10 +53807,6 @@ components:
|
||||
type: string
|
||||
description: Get metadata download URL
|
||||
readOnly: true
|
||||
url_issuer:
|
||||
type: string
|
||||
description: Get Issuer/EntityID URL
|
||||
readOnly: true
|
||||
url_sso_post:
|
||||
type: string
|
||||
description: Get SSO Post URL
|
||||
@@ -53889,7 +53840,6 @@ components:
|
||||
- name
|
||||
- pk
|
||||
- url_download_metadata
|
||||
- url_issuer
|
||||
- url_slo_post
|
||||
- url_slo_redirect
|
||||
- url_sso_init
|
||||
@@ -53957,10 +53907,10 @@ components:
|
||||
type: string
|
||||
description: Value of the audience restriction field of the assertion. When
|
||||
left empty, no audience restriction will be added.
|
||||
issuer_override:
|
||||
issuer:
|
||||
type: string
|
||||
description: Also known as EntityID. Providing a value overrides the default
|
||||
issuer generated by authentik.
|
||||
minLength: 1
|
||||
description: Also known as EntityID
|
||||
assertion_valid_not_before:
|
||||
type: string
|
||||
minLength: 1
|
||||
@@ -57623,15 +57573,6 @@ components:
|
||||
- identifier
|
||||
- source
|
||||
- user
|
||||
UserPasswordHashSetRequest:
|
||||
type: object
|
||||
description: Payload to set a users' password hash directly
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- password
|
||||
UserPasswordSetRequest:
|
||||
type: object
|
||||
description: Payload to set a users' password directly
|
||||
|
||||
@@ -39,7 +39,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
"9009": "9009",
|
||||
},
|
||||
environment={
|
||||
"SP_ENTITY_ID": provider.issuer_override,
|
||||
"SP_ENTITY_ID": provider.issuer,
|
||||
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
||||
"SP_METADATA_URL": metadata_url,
|
||||
**kwargs,
|
||||
@@ -68,7 +68,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
name=generate_id(),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer_override="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
@@ -147,7 +147,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
name=generate_id(),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer_override="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
@@ -226,7 +226,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
name=generate_id(),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer_override="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
@@ -321,7 +321,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
name=generate_id(),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer_override="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
@@ -415,7 +415,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
name=generate_id(),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer_override="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
@@ -503,7 +503,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
name=generate_id(),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer_override="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
signing_kp=create_test_cert(),
|
||||
@@ -553,7 +553,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
name=generate_id(),
|
||||
acs_url="http://localhost:9009/saml/acs",
|
||||
audience="authentik-e2e",
|
||||
issuer_override="authentik-e2e",
|
||||
issuer="authentik-e2e",
|
||||
sp_binding=SAMLBindings.POST,
|
||||
authorization_flow=authorization_flow,
|
||||
invalidation_flow=invalidation_flow,
|
||||
|
||||
@@ -77,8 +77,6 @@ export class FormFixture extends PageFixture {
|
||||
|
||||
/**
|
||||
* Search for a row containing the given text.
|
||||
*
|
||||
* @returns A locator for the row entry matching the query.
|
||||
*/
|
||||
public search = async (
|
||||
query: string,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NavigatorFixture } from "#e2e/fixtures/NavigatorFixture";
|
||||
import { PageFixture, PageFixtureInit } from "#e2e/fixtures/PageFixture";
|
||||
|
||||
import { expect, Page } from "@playwright/test";
|
||||
|
||||
export const GOOD_USERNAME = "test-admin@goauthentik.io";
|
||||
export const GOOD_PASSWORD = "test-runner";
|
||||
|
||||
@@ -13,8 +11,6 @@ export interface LoginInit {
|
||||
username?: string;
|
||||
password?: string;
|
||||
to?: URL | string;
|
||||
rememberMe?: boolean;
|
||||
page?: Page;
|
||||
}
|
||||
|
||||
export interface SessionFixtureInit extends PageFixtureInit {
|
||||
@@ -40,10 +36,6 @@ export class SessionFixture extends PageFixture {
|
||||
public $passwordStage = this.page.locator("ak-stage-password");
|
||||
public $passwordField = this.page.getByLabel("Password");
|
||||
|
||||
public $rememberMeCheckbox = this.page.getByRole("checkbox", {
|
||||
name: "Remember me on this device",
|
||||
});
|
||||
|
||||
/**
|
||||
* The button to submit the the login flow,
|
||||
* typically redirecting to the authenticated interface.
|
||||
@@ -74,45 +66,19 @@ export class SessionFixture extends PageFixture {
|
||||
/**
|
||||
* Log into the application.
|
||||
*/
|
||||
public async login(
|
||||
{
|
||||
username = GOOD_USERNAME,
|
||||
password = GOOD_PASSWORD,
|
||||
to = SessionFixture.pathname,
|
||||
rememberMe,
|
||||
}: LoginInit = {},
|
||||
page = this.page,
|
||||
): Promise<void> {
|
||||
public async login({
|
||||
username = GOOD_USERNAME,
|
||||
password = GOOD_PASSWORD,
|
||||
to = SessionFixture.pathname,
|
||||
}: LoginInit = {}) {
|
||||
this.logger.info("Logging in...");
|
||||
|
||||
const initialURL = new URL(page.url());
|
||||
const initialURL = new URL(this.page.url());
|
||||
|
||||
if (initialURL.pathname === SessionFixture.pathname) {
|
||||
this.logger.info("Skipping navigation because we're already in a authentication flow");
|
||||
} else {
|
||||
await page.goto(to.toString());
|
||||
}
|
||||
|
||||
if (typeof rememberMe === "boolean") {
|
||||
const rememberMeCheckboxVisible = await this.$rememberMeCheckbox.isVisible();
|
||||
|
||||
if (rememberMeCheckboxVisible) {
|
||||
if (rememberMe) {
|
||||
await this.$rememberMeCheckbox.check();
|
||||
|
||||
await expect(
|
||||
this.$rememberMeCheckbox,
|
||||
"Remember me checkbox is checked",
|
||||
).toBeChecked();
|
||||
} else {
|
||||
await this.$rememberMeCheckbox.uncheck();
|
||||
|
||||
await expect(
|
||||
this.$rememberMeCheckbox,
|
||||
"Remember me checkbox is unchecked",
|
||||
).not.toBeChecked();
|
||||
}
|
||||
}
|
||||
await this.page.goto(to.toString());
|
||||
}
|
||||
|
||||
await this.$usernameField.fill(username);
|
||||
@@ -136,7 +102,7 @@ export class SessionFixture extends PageFixture {
|
||||
|
||||
//#region Navigation
|
||||
|
||||
public async toLoginPage(page: Page = this.page) {
|
||||
await page.goto(SessionFixture.pathname);
|
||||
public async toLoginPage() {
|
||||
await this.page.goto(SessionFixture.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
225
web/package-lock.json
generated
225
web/package-lock.json
generated
@@ -78,7 +78,7 @@
|
||||
"globals": "^17.5.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^6.6.0",
|
||||
"knip": "^6.4.1",
|
||||
"lex": "^2025.11.0",
|
||||
"lit": "^3.3.2",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
@@ -2141,9 +2141,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
|
||||
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2224,9 +2224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-android-arm-eabi": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.126.0.tgz",
|
||||
"integrity": "sha512-svyoHt25J4741QJ5aa4R+h0iiBeSRt63Lr3aAZcxy2c/NeSE1IfDeMnSij6rIg7EjxkdlXzz613wUjeCeilBNA==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.121.0.tgz",
|
||||
"integrity": "sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2240,9 +2240,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-android-arm64": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.126.0.tgz",
|
||||
"integrity": "sha512-hPEBRKgplp1mG9GkINFsr4JVMDNrGJLOqfDaadTWpAoTnzYR5Rmv8RMvB3hJZpiNvbk1aacopdHUP1pggMQ/cw==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.121.0.tgz",
|
||||
"integrity": "sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2256,9 +2256,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-darwin-arm64": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.126.0.tgz",
|
||||
"integrity": "sha512-ccRpu9sdYmznePJQG5halhs0FW5tw5a8zRSoZXOzM1OjoeZ4jiRRruFiPclsD59edoVAK1l83dvfjWz1nQi6lg==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.121.0.tgz",
|
||||
"integrity": "sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2272,9 +2272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-darwin-x64": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.126.0.tgz",
|
||||
"integrity": "sha512-CHB4zVjNSKqx8Fw9pHowzQQnjjuq04i4Ng0Avj+DixlwhwAoMYqlFbocYIlbg+q3zOLGlm7vEHm83jqEMitnyg==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.121.0.tgz",
|
||||
"integrity": "sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2288,9 +2288,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-freebsd-x64": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.126.0.tgz",
|
||||
"integrity": "sha512-RQ3nEJdcDKBfBjmLJ3Vl1d0KQERPV1P8eUrnBm7+VTYyoaJSPLVFuPg1mlD1hk3n0/879VLFMfusFkBal4ssWQ==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.121.0.tgz",
|
||||
"integrity": "sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2304,9 +2304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.126.0.tgz",
|
||||
"integrity": "sha512-onipc2wCDA7Bauzb4KK1mab0GsEDf4ujiIfWECdnmY/2LlzAoX3xdQRLAUyEDB1kn3yilHBrkmXDdHluyHXxiw==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.121.0.tgz",
|
||||
"integrity": "sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2320,9 +2320,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.126.0.tgz",
|
||||
"integrity": "sha512-5BuJJPohrV5NJ8lmcYOMbfRCUGoYH5J9HZHeuqOLwkHXWAuPMN3X1h8bC/2mWjmosdbfTtmyIdX3spS/TkqKNg==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.121.0.tgz",
|
||||
"integrity": "sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2336,9 +2336,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-arm64-gnu": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.126.0.tgz",
|
||||
"integrity": "sha512-r2KApRgm2pOJaduRm6GOT8x0whcr67AyejNkSdzPt34GJ+Y3axcXN2mwlTs+8lfO/SSmpO5ZJGYiHYnxEE0jkw==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.121.0.tgz",
|
||||
"integrity": "sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2352,9 +2352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-arm64-musl": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.126.0.tgz",
|
||||
"integrity": "sha512-FQ+MMh7MT0Dr/u8+RWmWKlfoeWPQyHDbhhxJShJlYtROXXPHsRs9EvmQOZZ3sx4Nn7JU8NX+oyw2YzQ7anBJcA==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.121.0.tgz",
|
||||
"integrity": "sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2368,9 +2368,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.126.0.tgz",
|
||||
"integrity": "sha512-Wv/T8C98hRQhGTlx2XFyLn5raRMp9U1lOQD+YnXNgAr7wHbJJpZ8mDBU7Rw+M3WytGcGTFcr6kqgfyQeHVtLbQ==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.121.0.tgz",
|
||||
"integrity": "sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2384,9 +2384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.126.0.tgz",
|
||||
"integrity": "sha512-DHx1rT1zauW0ZbLHOiQh5AC9Xs3UkWx2XmfZHs+7nnWYr3sagrufoUQC+/XPwwjMIlCFXiFGM0sFh3TyOCZwqA==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.121.0.tgz",
|
||||
"integrity": "sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2400,9 +2400,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-riscv64-musl": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.126.0.tgz",
|
||||
"integrity": "sha512-umDc2mTShH0U2zcEYf8mIJ163seLJNn54ZUZYeI5jD4qlg9izPwoLrC2aNPKlMJTu6u/ysmQWiEvIiaAG+INkw==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.121.0.tgz",
|
||||
"integrity": "sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2416,9 +2416,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-s390x-gnu": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.126.0.tgz",
|
||||
"integrity": "sha512-PXXeWayclRtO1pxQEeCpiqIglQdhK2mAI2VX5xnsWdImzSB5GpoQ8TNw7vTCKk2k+GZuxl+q1knncidjCyUP9w==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.121.0.tgz",
|
||||
"integrity": "sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2432,9 +2432,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-x64-gnu": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.126.0.tgz",
|
||||
"integrity": "sha512-wzocjxm34TbB3bFlqG65JiLtvf6ZDg2ZxRkLLbgXwDQUNU+0MPjQN8zy/0jBKNA5fnPLk3XeVdZ7Uin+7+CVkg==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.121.0.tgz",
|
||||
"integrity": "sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2448,9 +2448,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-linux-x64-musl": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.126.0.tgz",
|
||||
"integrity": "sha512-e83uftP60jmkPs2+CW6T6A1GYzN2H6IumDAiTntv9WyHR73PI3ImHNBkYqnA3ukeKI3xjcCbhSh9QeJWmufxGQ==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.121.0.tgz",
|
||||
"integrity": "sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2464,9 +2464,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-openharmony-arm64": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.126.0.tgz",
|
||||
"integrity": "sha512-4WiOILHnPrTDY2/L4mE6PZCYwLN1d3ghma6BuTJ452CCgzRMt3uFplCtR+o3r9zdUWJYb370UizpI9CUcWXr1A==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.121.0.tgz",
|
||||
"integrity": "sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2480,27 +2480,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-wasm32-wasi": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.126.0.tgz",
|
||||
"integrity": "sha512-Y17hhnrQTrxgAxAyAq401vnN9URsAL4s5AjqpG1NDsXSlhe1yBNnns+rC2P6xcMoitgX5nKH2ryYt9oiFRlzLw==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.121.0.tgz",
|
||||
"integrity": "sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.9.2",
|
||||
"@emnapi/runtime": "1.9.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
"@napi-rs/wasm-runtime": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-win32-arm64-msvc": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.126.0.tgz",
|
||||
"integrity": "sha512-Znug1u1iRvT4VC3jANz6nhGBHsFwEFMxuimYpJFwMtsB6H5FcEoZRMmH26tHkSTD03JvDmG+gB65W3ajLjPcSw==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.121.0.tgz",
|
||||
"integrity": "sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2514,9 +2512,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-win32-ia32-msvc": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.126.0.tgz",
|
||||
"integrity": "sha512-qrw7mx5hFFTxVSXToOA40hpnjgNB/DJprZchtB4rDKNLKqkD3F26HbzaQeH1nxAKej0efSZfJd5Sw3qdtOLGhw==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.121.0.tgz",
|
||||
"integrity": "sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2530,9 +2528,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.126.0.tgz",
|
||||
"integrity": "sha512-ibB1s+mPUFXvS7MFJO2jpw/aCNs/P6ifnWlRyTYB+WYBpniOiCcHQQskZneJtwcjQMDRol3RGG3ihoYnzXSY4w==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.121.0.tgz",
|
||||
"integrity": "sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2546,9 +2544,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
|
||||
"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.121.0.tgz",
|
||||
"integrity": "sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
@@ -10006,9 +10004,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
|
||||
"version": "4.13.7",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
|
||||
"integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
@@ -11569,9 +11567,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-6.6.0.tgz",
|
||||
"integrity": "sha512-IT1YDiHyRctYYsuZNBd/ZiGoa7HmCaxs+ZrWxCfYjQKPG6QyRqMfkteqC+rBuMymBJeLXyBnRa7hn95O+sGG8Q==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-6.4.1.tgz",
|
||||
"integrity": "sha512-Ry+ywmDFSZvKp/jx7LxMgsZWRTs931alV84e60lh0Stf6kSRYqSIUTkviyyDFRcSO3yY1Kpbi83OirN+4lA2Xw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -11584,17 +11582,18 @@
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"fast-glob": "^3.3.3",
|
||||
"formatly": "^0.3.0",
|
||||
"get-tsconfig": "4.14.0",
|
||||
"get-tsconfig": "4.13.7",
|
||||
"jiti": "^2.6.0",
|
||||
"minimist": "^1.2.8",
|
||||
"oxc-parser": "^0.126.0",
|
||||
"oxc-parser": "^0.121.0",
|
||||
"oxc-resolver": "^11.19.1",
|
||||
"picomatch": "^4.0.4",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.1",
|
||||
"smol-toml": "^1.6.1",
|
||||
"strip-json-comments": "5.0.3",
|
||||
"tinyglobby": "^0.2.16",
|
||||
"unbash": "^2.2.0",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.1.11"
|
||||
@@ -11619,22 +11618,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/langium": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz",
|
||||
@@ -14283,12 +14266,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/oxc-parser": {
|
||||
"version": "0.126.0",
|
||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.126.0.tgz",
|
||||
"integrity": "sha512-FktCvLby/mOHyuijZt22+nOt10dS24gGUZE3XwIbUg7Kf4+rer3/5T7RgwzazlNuVsCjPloZ3p8E+4ONT3A8Kw==",
|
||||
"version": "0.121.0",
|
||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.121.0.tgz",
|
||||
"integrity": "sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "^0.126.0"
|
||||
"@oxc-project/types": "^0.121.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -14297,26 +14280,26 @@
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxc-parser/binding-android-arm-eabi": "0.126.0",
|
||||
"@oxc-parser/binding-android-arm64": "0.126.0",
|
||||
"@oxc-parser/binding-darwin-arm64": "0.126.0",
|
||||
"@oxc-parser/binding-darwin-x64": "0.126.0",
|
||||
"@oxc-parser/binding-freebsd-x64": "0.126.0",
|
||||
"@oxc-parser/binding-linux-arm-gnueabihf": "0.126.0",
|
||||
"@oxc-parser/binding-linux-arm-musleabihf": "0.126.0",
|
||||
"@oxc-parser/binding-linux-arm64-gnu": "0.126.0",
|
||||
"@oxc-parser/binding-linux-arm64-musl": "0.126.0",
|
||||
"@oxc-parser/binding-linux-ppc64-gnu": "0.126.0",
|
||||
"@oxc-parser/binding-linux-riscv64-gnu": "0.126.0",
|
||||
"@oxc-parser/binding-linux-riscv64-musl": "0.126.0",
|
||||
"@oxc-parser/binding-linux-s390x-gnu": "0.126.0",
|
||||
"@oxc-parser/binding-linux-x64-gnu": "0.126.0",
|
||||
"@oxc-parser/binding-linux-x64-musl": "0.126.0",
|
||||
"@oxc-parser/binding-openharmony-arm64": "0.126.0",
|
||||
"@oxc-parser/binding-wasm32-wasi": "0.126.0",
|
||||
"@oxc-parser/binding-win32-arm64-msvc": "0.126.0",
|
||||
"@oxc-parser/binding-win32-ia32-msvc": "0.126.0",
|
||||
"@oxc-parser/binding-win32-x64-msvc": "0.126.0"
|
||||
"@oxc-parser/binding-android-arm-eabi": "0.121.0",
|
||||
"@oxc-parser/binding-android-arm64": "0.121.0",
|
||||
"@oxc-parser/binding-darwin-arm64": "0.121.0",
|
||||
"@oxc-parser/binding-darwin-x64": "0.121.0",
|
||||
"@oxc-parser/binding-freebsd-x64": "0.121.0",
|
||||
"@oxc-parser/binding-linux-arm-gnueabihf": "0.121.0",
|
||||
"@oxc-parser/binding-linux-arm-musleabihf": "0.121.0",
|
||||
"@oxc-parser/binding-linux-arm64-gnu": "0.121.0",
|
||||
"@oxc-parser/binding-linux-arm64-musl": "0.121.0",
|
||||
"@oxc-parser/binding-linux-ppc64-gnu": "0.121.0",
|
||||
"@oxc-parser/binding-linux-riscv64-gnu": "0.121.0",
|
||||
"@oxc-parser/binding-linux-riscv64-musl": "0.121.0",
|
||||
"@oxc-parser/binding-linux-s390x-gnu": "0.121.0",
|
||||
"@oxc-parser/binding-linux-x64-gnu": "0.121.0",
|
||||
"@oxc-parser/binding-linux-x64-musl": "0.121.0",
|
||||
"@oxc-parser/binding-openharmony-arm64": "0.121.0",
|
||||
"@oxc-parser/binding-wasm32-wasi": "0.121.0",
|
||||
"@oxc-parser/binding-win32-arm64-msvc": "0.121.0",
|
||||
"@oxc-parser/binding-win32-ia32-msvc": "0.121.0",
|
||||
"@oxc-parser/binding-win32-x64-msvc": "0.121.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oxc-resolver": {
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
"globals": "^17.5.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^6.6.0",
|
||||
"knip": "^6.4.1",
|
||||
"lex": "^2025.11.0",
|
||||
"lit": "^3.3.2",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
|
||||
@@ -201,15 +201,6 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="metaHide"
|
||||
?checked=${this.instance?.metaHide ?? false}
|
||||
label=${msg("Hide from My applications")}
|
||||
help=${msg(
|
||||
"If checked, this application will not be shown on the user's My applications page.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-file-search-input
|
||||
name="metaIcon"
|
||||
label=${msg("Icon")}
|
||||
|
||||
@@ -42,7 +42,7 @@ const renderSAMLOverview: ProviderOverview<SAMLProvider> = (provider) => {
|
||||
return renderSummary("SAML", provider.name, [
|
||||
[msg("ACS URL"), provider.acsUrl],
|
||||
[msg("Audience"), provider.audience || "-"],
|
||||
[msg("Issuer"), provider.urlIssuer],
|
||||
[msg("Issuer"), provider.issuer],
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -187,15 +187,6 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="metaHide"
|
||||
?checked=${app.metaHide ?? false}
|
||||
label=${msg("Hide from My applications")}
|
||||
help=${msg(
|
||||
"If checked, this application will not be shown on the user's My applications page.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-file-search-input
|
||||
name="metaIcon"
|
||||
label=${msg("Icon")}
|
||||
|
||||
@@ -38,20 +38,22 @@ export interface SelectedFeatureEventDetail {
|
||||
|
||||
@customElement("ak-map")
|
||||
export class Map extends OlMap {
|
||||
public styles: CSSResult[] = [
|
||||
OlMap.styles,
|
||||
OL,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<style>
|
||||
${OL}
|
||||
</style>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<div id="map"></div>
|
||||
<slot></slot>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,6 @@ import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { formatUserDisplayName } from "#common/users";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
@@ -76,7 +75,7 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
|
||||
}
|
||||
|
||||
protected override rowLabel(item: GoogleWorkspaceProviderUser): string {
|
||||
return formatUserDisplayName(item.userObj);
|
||||
return item.userObj.name || item.userObj.username;
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
|
||||
@@ -3,7 +3,6 @@ import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncObjectForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { formatUserDisplayName } from "#common/users";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
@@ -76,7 +75,7 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
|
||||
}
|
||||
|
||||
protected override rowLabel(item: MicrosoftEntraProviderUser): string {
|
||||
return formatUserDisplayName(item.userObj);
|
||||
return item.userObj.name || item.userObj.username;
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
|
||||
@@ -196,6 +196,15 @@ export function renderForm({
|
||||
required
|
||||
.errorMessages=${errors.acsUrl}
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
label=${msg("Issuer")}
|
||||
input-hint="code"
|
||||
name="issuer"
|
||||
value="${provider.issuer || "authentik"}"
|
||||
required
|
||||
.errorMessages=${errors.issuer}
|
||||
help=${msg("Also known as Entity ID.")}
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
name="audience"
|
||||
label=${msg("Audience")}
|
||||
@@ -427,15 +436,6 @@ export function renderForm({
|
||||
"When using IDP-initiated logins, the relay state will be set to this value.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
label=${msg("EntityID/Issuer override")}
|
||||
name="issuerOverride"
|
||||
value="${ifDefined(provider.issuerOverride ?? undefined)}"
|
||||
.errorMessages=${errors.issuerOverride}
|
||||
help=${msg(
|
||||
"Sets a custom EntityID/Issuer to override the authentik generated default.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-radio-input
|
||||
label=${msg("Service Provider Binding")}
|
||||
name="spBinding"
|
||||
|
||||
@@ -350,7 +350,7 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.provider.issuerOverride}
|
||||
${this.provider.issuer}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
@@ -385,7 +385,7 @@ export class SAMLProviderViewPage extends AKElement {
|
||||
class="pf-c-form-control"
|
||||
readonly
|
||||
type="text"
|
||||
value="${ifDefined(this.provider?.urlIssuer)}"
|
||||
value="${ifDefined(this.provider?.issuer)}"
|
||||
/>
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
|
||||
@@ -4,7 +4,6 @@ import "#elements/sync/SyncObjectForm";
|
||||
import "#admin/common/ak-flow-search/ak-flow-search-no-default";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { formatUserDisplayName } from "#common/users";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
@@ -74,7 +73,7 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
|
||||
}
|
||||
|
||||
protected override rowLabel(item: SCIMProviderUser): string {
|
||||
return formatUserDisplayName(item.userObj);
|
||||
return item.userObj.name || item.userObj.username;
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { formatUserDisplayName } from "#common/users";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
@@ -26,7 +25,7 @@ export class SCIMSourceUserList extends Table<SCIMSourceUser> {
|
||||
}
|
||||
|
||||
protected override rowLabel(item: SCIMSourceUser): string {
|
||||
return formatUserDisplayName(item.userObj);
|
||||
return item.userObj.name || item.userObj.username;
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
|
||||
@@ -2,30 +2,20 @@ import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
import { FocusTarget } from "#elements/utils/focus";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { CoreApi, UserPasswordSetRequest } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-user-password-form")
|
||||
export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
public static shadowRootOptions: ShadowRootInit = {
|
||||
...Form.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public static override verboseName = msg("Password");
|
||||
public static override verboseNamePlural = msg("Passwords");
|
||||
public static override submittingVerb = msg("Setting");
|
||||
public override submitLabel = msg("Set Password");
|
||||
|
||||
protected autofocusTarget = new FocusTarget<HTMLInputElement>();
|
||||
|
||||
@@ -33,9 +23,6 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
|
||||
//#region Properties
|
||||
|
||||
public override submitLabel = msg("Set Password");
|
||||
public override successMessage = msg("Successfully updated password.");
|
||||
|
||||
@property({ type: Number })
|
||||
public instancePk?: number;
|
||||
|
||||
@@ -43,15 +30,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
public label = msg("New Password");
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder = msg("Type a new password...");
|
||||
public placeholder = msg("New Password");
|
||||
|
||||
@property({ type: String, useDefault: true })
|
||||
public username: string | null = null;
|
||||
@property({ type: String })
|
||||
public username?: string;
|
||||
|
||||
@property({ type: String, useDefault: true })
|
||||
public email: string | null = null;
|
||||
|
||||
public override size = PFSize.Medium;
|
||||
@property({ type: String })
|
||||
public email?: string;
|
||||
|
||||
/**
|
||||
* The autocomplete attribute to use for the password field.
|
||||
@@ -65,15 +50,17 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
|
||||
//#endregion
|
||||
|
||||
public override getSuccessMessage(): string {
|
||||
return msg("Successfully updated password.");
|
||||
}
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("focus", this.autofocusTarget.toEventListener());
|
||||
}
|
||||
|
||||
public override firstUpdated(): void {
|
||||
requestAnimationFrame(() => {
|
||||
this.focus();
|
||||
});
|
||||
this.focus();
|
||||
}
|
||||
|
||||
protected override async send(data: UserPasswordSetRequest): Promise<void> {
|
||||
@@ -107,26 +94,17 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
||||
/>`
|
||||
: nothing}
|
||||
|
||||
<ak-form-element-horizontal required name="password">
|
||||
${AKLabel(
|
||||
{
|
||||
slot: "label",
|
||||
className: "pf-c-form__group-label",
|
||||
htmlFor: "password",
|
||||
required: true,
|
||||
},
|
||||
this.label,
|
||||
)}
|
||||
<ak-form-element-horizontal label=${this.label} required name="password">
|
||||
<input
|
||||
autofocus
|
||||
${this.autofocusTarget.toRef()}
|
||||
id="password"
|
||||
type="password"
|
||||
value=""
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
placeholder=${ifPresent(this.placeholder || this.label)}
|
||||
autocomplete=${ifPresent(this.autocomplete)}
|
||||
placeholder=${ifDefined(this.placeholder || this.label)}
|
||||
aria-label=${this.label}
|
||||
autocomplete=${ifDefined(this.autocomplete)}
|
||||
/>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { formatUserDisplayName } from "#common/users";
|
||||
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { LitFC } from "#elements/types";
|
||||
|
||||
@@ -54,7 +52,7 @@ export const RecoveryButtons: LitFC<RecoveryButtonsProps> = ({
|
||||
class="pf-c-button pf-m-secondary ${buttonClasses || ""}"
|
||||
type="button"
|
||||
${modalInvoker(UserPasswordForm, {
|
||||
headline: msg(str`Update ${formatUserDisplayName(user)}'s password`),
|
||||
headline: msg(str`Update ${user.name || user.username}'s password`),
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
instancePk: user.pk,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user