Compare commits

..

2 Commits

Author SHA1 Message Date
Marcelo Elizeche Landó
7325bfde36 add monkeytype_config.py 2025-07-23 13:10:03 -03:00
Marcelo Elizeche Landó
6383d0bfc0 Add types using typemonkey 2025-07-18 02:28:56 -03:00
40 changed files with 818 additions and 1283 deletions

View File

@@ -75,7 +75,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 4: Download uv
FROM ghcr.io/astral-sh/uv:0.8.0 AS uv
FROM ghcr.io/astral-sh/uv:0.7.21 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base

View File

@@ -77,7 +77,7 @@ class GroupSerializer(ModelSerializer):
return None
return GroupMemberSerializer(instance.users, many=True).data
def validate_parent(self, parent: Group | None):
def validate_parent(self, parent: Group | None) -> None:
"""Validate group parent (if set), ensuring the parent isn't itself"""
if not self.instance or not parent:
return parent
@@ -85,7 +85,7 @@ class GroupSerializer(ModelSerializer):
raise ValidationError(_("Cannot set group as parent of itself."))
return parent
def validate_is_superuser(self, superuser: bool):
def validate_is_superuser(self, superuser: bool) -> bool:
"""Ensure that the user creating this group has permissions to set the superuser flag"""
request: Request = self.context.get("request", None)
if not request:
@@ -210,7 +210,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_users", bool, default=True),
]
)
def list(self, request, *args, **kwargs):
def list(self, request: Request, *args, **kwargs) -> Response:
return super().list(request, *args, **kwargs)
@extend_schema(
@@ -218,7 +218,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_users", bool, default=True),
]
)
def retrieve(self, request, *args, **kwargs):
def retrieve(self, request: Request, *args, **kwargs) -> Response:
return super().retrieve(request, *args, **kwargs)
@permission_required("authentik_core.add_user_to_group")

View File

@@ -5,6 +5,7 @@ from django.db.models.query import Q
from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from model_utils.managers import InheritanceQuerySet
from rest_framework import mixins
from rest_framework.fields import ReadOnlyField, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
@@ -99,5 +100,5 @@ class ProviderViewSet(
"application__name",
]
def get_queryset(self): # pragma: no cover
def get_queryset(self) -> InheritanceQuerySet: # pragma: no cover
return Provider.objects.select_subclasses()

View File

@@ -3,6 +3,7 @@
from collections.abc import Iterable
from drf_spectacular.utils import OpenApiResponse, extend_schema
from model_utils.managers import InheritanceQuerySet
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
@@ -88,7 +89,7 @@ class SourceViewSet(
search_fields = ["slug", "name"]
filterset_fields = ["slug", "name", "managed", "pbm_uuid"]
def get_queryset(self): # pragma: no cover
def get_queryset(self) -> InheritanceQuerySet: # pragma: no cover
return Source.objects.select_subclasses()
@permission_required("authentik_core.change_source")

View File

@@ -2,6 +2,7 @@
from typing import Any
from django.db.models.query import QuerySet
from django.utils.timezone import now
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user
@@ -41,7 +42,7 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)
def validate_user(self, user: User):
def validate_user(self, user: User) -> User:
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
@@ -138,13 +139,13 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
owner_field = "user"
rbac_allow_create_without_perm = True
def get_queryset(self):
def get_queryset(self) -> QuerySet:
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)
def perform_create(self, serializer: TokenSerializer):
def perform_create(self, serializer: TokenSerializer) -> Token:
if not self.request.user.is_superuser:
instance = serializer.save(
user=self.request.user,

View File

@@ -21,6 +21,7 @@ from django_filters.filters import (
UUIDFilter,
)
from django_filters.filterset import FilterSet
from djangoql.schema import BoolField, StrField
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
@@ -72,8 +73,10 @@ from authentik.core.models import (
Token,
TokenIntents,
User,
UserQuerySet,
UserTypes,
)
from authentik.enterprise.search.fields import ChoiceSearchField, JSONSearchField
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
@@ -349,7 +352,7 @@ class UsersFilter(FilterSet):
queryset=Group.objects.all().order_by("name"),
)
def filter_is_superuser(self, queryset, name, value):
def filter_is_superuser(self, queryset: UserQuerySet, name: str, value: bool) -> UserQuerySet:
if value:
return queryset.filter(ak_groups__is_superuser=True).distinct()
return queryset.exclude(ak_groups__is_superuser=True).distinct()
@@ -395,7 +398,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
filterset_class = UsersFilter
search_fields = ["username", "name", "is_active", "email", "uuid", "attributes"]
def get_ql_fields(self):
def get_ql_fields(self) -> list[StrField | BoolField | ChoiceSearchField | JSONSearchField]:
from djangoql.schema import BoolField, StrField
from authentik.enterprise.search.fields import ChoiceSearchField, JSONSearchField
@@ -410,7 +413,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
JSONSearchField(User, "attributes", suggest_nested=False),
]
def get_queryset(self):
def get_queryset(self) -> UserQuerySet:
base_qs = User.objects.all().exclude_anonymous()
if self.serializer_class(context={"request": self.request})._should_include_groups:
base_qs = base_qs.prefetch_related("ak_groups")
@@ -421,10 +424,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_groups", bool, default=True),
]
)
def list(self, request, *args, **kwargs):
def list(self, request: Request, *args, **kwargs) -> Response:
return super().list(request, *args, **kwargs)
def _create_recovery_link(self, for_email=False) -> tuple[str, Token]:
def _create_recovery_link(self, for_email: bool = False) -> tuple[str, Token]:
"""Create a recovery link (when the current brand has a recovery flow set),
that can either be shown to an admin or sent to the user directly"""
brand: Brand = self.request._request.brand

View File

@@ -42,7 +42,7 @@ class JSONExtension(OpenApiSerializerFieldExtension):
target_class = "authentik.core.api.utils.JSONDictField"
def map_serializer_field(self, auto_schema, direction):
def map_serializer_field(self, auto_schema, direction: str) -> dict[str, str]:
return build_basic_type(OpenApiTypes.OBJECT)
@@ -52,7 +52,7 @@ class ModelSerializer(BaseModelSerializer):
serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy()
serializer_field_mapping[models.JSONField] = JSONDictField
def create(self, validated_data):
def create(self, validated_data: dict[str, Any]):
instance = super().create(validated_data)
request = self.context.get("request")
@@ -61,7 +61,7 @@ class ModelSerializer(BaseModelSerializer):
return instance
def update(self, instance: Model, validated_data):
def update(self, instance: Model, validated_data: dict[str, Any]):
raise_errors_on_nested_writes("update", self, validated_data)
info = model_meta.get_field_info(instance)

View File

@@ -7,6 +7,7 @@ from uuid import uuid4
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpRequest, HttpResponse
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject
@@ -14,6 +15,8 @@ from django.utils.translation import override
from sentry_sdk.api import set_tag
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
from authentik.core.models import User
SESSION_KEY_IMPERSONATE_USER = "authentik/impersonate/user"
SESSION_KEY_IMPERSONATE_ORIGINAL_USER = "authentik/impersonate/original_user"
RESPONSE_HEADER_ID = "X-authentik-id"
@@ -25,7 +28,7 @@ CTX_HOST = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + "host", default=None)
CTX_AUTH_VIA = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
def get_user(request):
def get_user(request: WSGIRequest) -> AnonymousUser | User:
if not hasattr(request, "_cached_user"):
user = None
if (authenticated_session := request.session.get("authenticatedsession", None)) is not None:
@@ -46,7 +49,7 @@ async def aget_user(request):
class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
def process_request(self, request: WSGIRequest):
if not hasattr(request, "session"):
raise ImproperlyConfigured(
"The Django authentication middleware requires session "

View File

@@ -11,6 +11,7 @@ from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.contrib.sessions.base_session import AbstractBaseSession
from django.core.handlers.wsgi import WSGIRequest
from django.db import models
from django.db.models import Q, QuerySet, options
from django.db.models.constants import LOOKUP_SEP
@@ -22,6 +23,7 @@ from django_cte import CTE, with_cte
from guardian.conf import settings
from guardian.mixins import GuardianUserMixin
from model_utils.managers import InheritanceManager
from rest_framework.request import Request
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
@@ -137,7 +139,7 @@ class AttributesMixin(models.Model):
class GroupQuerySet(QuerySet):
def with_children_recursive(self):
def with_children_recursive(self) -> "GroupQuerySet":
"""Recursively get all groups that have the current queryset as parents
or are indirectly related."""
@@ -210,7 +212,7 @@ class Group(SerializerModel, AttributesMixin):
("disable_group_superuser", _("Disable superuser status")),
]
def __str__(self):
def __str__(self) -> str:
return f"Group {self.name}"
@property
@@ -241,7 +243,7 @@ class Group(SerializerModel, AttributesMixin):
class UserQuerySet(models.QuerySet):
"""User queryset"""
def exclude_anonymous(self):
def exclude_anonymous(self) -> "UserQuerySet":
"""Exclude anonymous user"""
return self.exclude(**{User.USERNAME_FIELD: settings.ANONYMOUS_USER_NAME})
@@ -249,7 +251,7 @@ class UserQuerySet(models.QuerySet):
class UserManager(DjangoUserManager):
"""User manager that doesn't assign is_superuser and is_staff"""
def get_queryset(self):
def get_queryset(self) -> UserQuerySet:
"""Create special user queryset"""
return UserQuerySet(self.model, using=self._db)
@@ -295,7 +297,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
models.Index(fields=["type"]),
]
def __str__(self):
def __str__(self) -> str:
return self.username
@staticmethod
@@ -360,7 +362,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
"""superuser == staff user"""
return self.is_superuser # type: ignore
def set_password(self, raw_password, signal=True, sender=None, request=None):
def set_password(
self,
raw_password: str,
signal: bool = True,
sender: None = None,
request: WSGIRequest | Request | None = None,
) -> None:
if self.pk and signal:
from authentik.core.signals import password_changed
@@ -479,7 +487,7 @@ class Provider(SerializerModel):
"""Get serializer for this model"""
raise NotImplementedError
def __str__(self):
def __str__(self) -> str:
return str(self.name)
@@ -611,7 +619,7 @@ class Application(SerializerModel, PolicyBindingModel):
)
return getattr(providers.first(), provider_type._meta.model_name)
def __str__(self):
def __str__(self) -> str:
return str(self.name)
class Meta:
@@ -631,7 +639,7 @@ class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingMode
verbose_name_plural = _("Application Entitlements")
unique_together = (("app", "name"),)
def __str__(self):
def __str__(self) -> str:
return f"Application Entitlement {self.name} for app {self.app_id}"
@property
@@ -640,7 +648,7 @@ class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingMode
return ApplicationEntitlementSerializer
def supported_policy_binding_targets(self):
def supported_policy_binding_targets(self) -> list[str]:
return ["group", "user"]
@@ -812,7 +820,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
return {}
raise NotImplementedError
def __str__(self):
def __str__(self) -> str:
return str(self.name)
class Meta:
@@ -895,7 +903,7 @@ class ExpiringModel(models.Model):
models.Index(fields=["expiring", "expires"]),
]
def expire_action(self, *args, **kwargs):
def expire_action(self, *args, **kwargs) -> tuple[int, dict[str, int]]:
"""Handler which is called when this object is expired. By
default the object is deleted. This is less efficient compared
to bulk deleting objects, but classes like Token() need to change
@@ -958,7 +966,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
("set_token_key", _("Set a token's key")),
]
def __str__(self):
def __str__(self) -> str:
description = f"{self.identifier}"
if self.expiring:
description += f" (expires={self.expires})"
@@ -1023,7 +1031,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
except Exception as exc:
raise PropertyMappingExpressionException(exc, self) from exc
def __str__(self):
def __str__(self) -> str:
return f"Property Mapping {self.name}"
class Meta:
@@ -1051,7 +1059,7 @@ class Session(ExpiringModel, AbstractBaseSession):
]
default_permissions = []
def __str__(self):
def __str__(self) -> str:
return self.session_key
class Keys(StrEnum):

View File

@@ -1,6 +1,7 @@
"""authentik sessions engine"""
import pickle # nosec
from typing import Any
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
from django.contrib.sessions.backends.db import SessionStore as SessionBase
@@ -9,13 +10,19 @@ from django.utils import timezone
from django.utils.functional import cached_property
from structlog.stdlib import get_logger
from authentik.core.models import Session
from authentik.root.middleware import ClientIPMiddleware
LOGGER = get_logger()
class SessionStore(SessionBase):
def __init__(self, session_key=None, last_ip=None, last_user_agent=""):
def __init__(
self,
session_key: str | None = None,
last_ip: str | None = None,
last_user_agent: str = "",
):
super().__init__(session_key)
self._create_kwargs = {
"last_ip": last_ip or ClientIPMiddleware.default_ip,
@@ -23,16 +30,16 @@ class SessionStore(SessionBase):
}
@classmethod
def get_model_class(cls):
def get_model_class(cls) -> type[Session]:
from authentik.core.models import Session
return Session
@cached_property
def model_fields(self):
def model_fields(self) -> list[str]:
return [k.value for k in self.model.Keys]
def _get_session_from_db(self):
def _get_session_from_db(self) -> Session:
try:
return (
self.model.objects.select_related(
@@ -74,10 +81,10 @@ class SessionStore(SessionBase):
LOGGER.warning(str(exc))
self._session_key = None
def encode(self, session_dict):
def encode(self, session_dict: dict[str, Any]) -> bytes:
return pickle.dumps(session_dict, protocol=pickle.HIGHEST_PROTOCOL)
def decode(self, session_data):
def decode(self, session_data: bytes) -> dict[str, Any]:
try:
return pickle.loads(session_data) # nosec
except pickle.PickleError:
@@ -86,7 +93,7 @@ class SessionStore(SessionBase):
pass
return {}
def load(self):
def load(self) -> dict[str, Any]:
s = self._get_session_from_db()
if s:
return {
@@ -108,7 +115,7 @@ class SessionStore(SessionBase):
else:
return {}
def create_model_instance(self, data):
def create_model_instance(self, data: dict[str, Any]) -> Session:
args = {
"session_key": self._get_or_create_session_key(),
"expires": self.get_expiry_date(),

View File

@@ -3,6 +3,7 @@
from django.http.response import (
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponseNotAllowed,
HttpResponseNotFound,
HttpResponseServerError,
)
@@ -61,6 +62,6 @@ class ServerErrorView(TemplateView):
response_class = ServerErrorTemplateResponse
template_name = "if/error.html"
def dispatch(self, *args, **kwargs): # pragma: no cover
def dispatch(self, *args, **kwargs) -> HttpResponseNotAllowed: # pragma: no cover
"""Little wrapper so django accepts this function"""
return super().dispatch(*args, **kwargs)

4
go.mod
View File

@@ -17,7 +17,7 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.3
github.com/grafana/pyroscope-go v1.2.4
github.com/grafana/pyroscope-go v1.2.3
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
@@ -29,7 +29,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025063.6
goauthentik.io/api/v3 v3.2025063.5
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.16.0

8
go.sum
View File

@@ -180,8 +180,8 @@ github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2e
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.2.4 h1:B22GMXz+O0nWLatxLuaP7o7L9dvP0clLvIpmeEQQM0Q=
github.com/grafana/pyroscope-go v1.2.4/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
github.com/grafana/pyroscope-go v1.2.3 h1:Rp8mjqqGqmRDvV6XYmuedUAv7wVnQJK/M1pBt6uNwxU=
github.com/grafana/pyroscope-go v1.2.3/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -298,8 +298,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025063.6 h1:TFMnE0bXiWZ5oVYrnxDLpS+pnGNv+KIjLmZHT5qzpcM=
goauthentik.io/api/v3 v3.2025063.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025063.5 h1:j5el9/qI/72Q5x5QAiMzgQTswMj2TK3h74OaBcFEtkI=
goauthentik.io/api/v3 v3.2025063.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@@ -2,7 +2,6 @@ package radius
import (
"encoding/base64"
"errors"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
@@ -12,7 +11,9 @@ import (
"layeh.com/radius/rfc2865"
)
func (rs *RadiusServer) Handle_AccessRequest_PAP_Auth(r *RadiusRequest, username, password string) (*radius.Packet, error) {
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
username := rfc2865.UserName_GetString(r.Packet)
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
"username": username,
"client": r.RemoteAddr(),
@@ -22,64 +23,67 @@ func (rs *RadiusServer) Handle_AccessRequest_PAP_Auth(r *RadiusRequest, username
fe.Params.Add("goauthentik.io/outpost/radius", "true")
fe.Answers[flow.StageIdentification] = username
fe.SetSecrets(password, r.pi.MFASupport)
fe.SetSecrets(rfc2865.UserPassword_GetString(r.Packet), r.pi.MFASupport)
passed, err := fe.Execute()
if err != nil {
r.Log().WithField("username", username).WithError(err).Warning("failed to execute flow")
return nil, errors.New("flow_error")
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "flow_error",
"app": r.pi.appSlug,
}).Inc()
_ = w.Write(r.Response(radius.CodeAccessReject))
return
}
if !passed {
return nil, errors.New("invalid_credentials")
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "invalid_credentials",
"app": r.pi.appSlug,
}).Inc()
_ = w.Write(r.Response(radius.CodeAccessReject))
return
}
access, _, err := fe.ApiClient().OutpostsApi.OutpostsRadiusAccessCheck(
r.Context(), r.pi.providerId,
).AppSlug(r.pi.appSlug).Execute()
if err != nil {
r.Log().WithField("username", username).WithError(err).Warning("failed to check access")
return nil, errors.New("access_check_fail")
_ = w.Write(r.Response(radius.CodeAccessReject))
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "access_check_fail",
"app": r.pi.appSlug,
}).Inc()
return
}
if !access.Access.Passing {
r.Log().WithField("username", username).Info("Access denied for user")
return nil, errors.New("access_denied")
_ = w.Write(r.Response(radius.CodeAccessReject))
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "access_denied",
"app": r.pi.appSlug,
}).Inc()
return
}
res := r.Response(radius.CodeAccessAccept)
defer func() { _ = w.Write(res) }()
if !access.HasAttributes() {
r.Log().Debug("No attributes")
return res, nil
return
}
rawData, err := base64.StdEncoding.DecodeString(access.GetAttributes())
if err != nil {
r.Log().WithError(err).Warning("failed to decode attributes from core")
return nil, errors.New("attribute_decode_failed")
return
}
p, err := radius.Parse(rawData, r.pi.SharedSecret)
if err != nil {
r.Log().WithError(err).Warning("failed to parse attributes from core")
return nil, errors.New("attribute_parse_failed")
}
for _, attr := range p.Attributes {
res.Add(attr.Type, attr.Attribute)
}
return res, nil
}
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
username := rfc2865.UserName_GetString(r.Packet)
password := rfc2865.UserPassword_GetString(r.Packet)
res, err := rs.Handle_AccessRequest_PAP_Auth(r, username, password)
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": err.Error(),
"app": r.pi.appSlug,
}).Inc()
_ = w.Write(r.Reject())
return
}
err = r.setMessageAuthenticator(res)
if err != nil {
rs.log.WithError(err).Warning("failed to set message authenticator")
}
_ = w.Write(res)
}

View File

@@ -1,12 +1,8 @@
package radius
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha512"
"encoding/hex"
"errors"
"time"
"github.com/getsentry/sentry-go"
@@ -17,11 +13,6 @@ import (
"goauthentik.io/internal/outpost/radius/metrics"
"goauthentik.io/internal/utils"
"layeh.com/radius"
"layeh.com/radius/rfc2869"
)
var (
ErrInvalidMessageAuthenticator = errors.New("invalid message authenticator")
)
type RadiusRequest struct {
@@ -44,41 +35,6 @@ func (r *RadiusRequest) ID() string {
return r.id
}
func (r *RadiusRequest) validateMessageAuthenticator() error {
mauth := rfc2869.MessageAuthenticator_Get(r.Packet)
hash := hmac.New(md5.New, r.Secret)
encode, err := r.MarshalBinary()
if err != nil {
return err
}
hash.Write(encode)
if bytes.Equal(mauth, hash.Sum(nil)) {
return ErrInvalidMessageAuthenticator
}
return nil
}
func (r *RadiusRequest) setMessageAuthenticator(rp *radius.Packet) error {
_ = rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16))
hash := hmac.New(md5.New, rp.Secret)
encode, err := rp.MarshalBinary()
if err != nil {
return err
}
hash.Write(encode)
_ = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil))
return nil
}
func (r *RadiusRequest) Reject() *radius.Packet {
res := r.Response(radius.CodeAccessReject)
err := r.setMessageAuthenticator(res)
if err != nil {
r.log.WithError(err).Warning("failed to set message authenticator")
}
return res
}
func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
span := sentry.StartSpan(r.Context(), "authentik.providers.radius.connect",
sentry.WithTransactionName("authentik.providers.radius.connect"))
@@ -103,11 +59,6 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
rl.Info("Radius Request")
if err := nr.validateMessageAuthenticator(); err != nil {
rl.WithError(err).Warning("Invalid message authenticator")
return
}
// Lookup provider by shared secret
var pi *ProviderInstance
for _, p := range rs.providers {
@@ -121,7 +72,7 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
hs := sha512.Sum512([]byte(r.Secret))
bs := hex.EncodeToString(hs[:])
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
_ = w.Write(nr.Reject())
_ = w.Write(r.Response(radius.CodeAccessReject))
return
}
nr.pi = pi

20
monkeytype_config.py Normal file
View File

@@ -0,0 +1,20 @@
# Standard Library
import os
from collections.abc import Iterator
from contextlib import contextmanager
# 3rd-party
from monkeytype.config import DefaultConfig
class MonkeyConfig(DefaultConfig):
@contextmanager
def cli_context(self, command: str) -> Iterator[None]:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
import django
django.setup()
yield
CONFIG = MonkeyConfig()

View File

@@ -5954,9 +5954,9 @@
}
},
"node_modules/compression": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5964,7 +5964,7 @@
"compressible": "~2.0.18",
"debug": "2.6.9",
"negotiator": "~0.6.4",
"on-headers": "~1.1.0",
"on-headers": "~1.0.2",
"safe-buffer": "5.2.1",
"vary": "~1.1.2"
},
@@ -12708,9 +12708,9 @@
}
},
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -132,9 +132,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
"integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",
"cpu": [
"ppc64"
],
@@ -149,9 +149,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",
"integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",
"cpu": [
"arm"
],
@@ -166,9 +166,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",
"integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",
"cpu": [
"arm64"
],
@@ -183,9 +183,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",
"integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",
"cpu": [
"x64"
],
@@ -200,9 +200,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",
"integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",
"cpu": [
"arm64"
],
@@ -217,9 +217,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",
"integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",
"cpu": [
"x64"
],
@@ -234,9 +234,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",
"integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",
"cpu": [
"arm64"
],
@@ -251,9 +251,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",
"integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",
"cpu": [
"x64"
],
@@ -268,9 +268,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",
"integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",
"cpu": [
"arm"
],
@@ -285,9 +285,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",
"integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",
"cpu": [
"arm64"
],
@@ -302,9 +302,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",
"integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",
"cpu": [
"ia32"
],
@@ -319,9 +319,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",
"integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",
"cpu": [
"loong64"
],
@@ -336,9 +336,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",
"integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",
"cpu": [
"mips64el"
],
@@ -353,9 +353,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",
"integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",
"cpu": [
"ppc64"
],
@@ -370,9 +370,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",
"integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",
"cpu": [
"riscv64"
],
@@ -387,9 +387,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",
"integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",
"cpu": [
"s390x"
],
@@ -404,9 +404,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",
"integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",
"cpu": [
"x64"
],
@@ -421,9 +421,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",
"integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",
"cpu": [
"arm64"
],
@@ -438,9 +438,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",
"integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",
"cpu": [
"x64"
],
@@ -455,9 +455,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",
"integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",
"cpu": [
"arm64"
],
@@ -472,9 +472,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",
"integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",
"cpu": [
"x64"
],
@@ -489,9 +489,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz",
"integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==",
"cpu": [
"arm64"
],
@@ -506,9 +506,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",
"integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",
"cpu": [
"x64"
],
@@ -523,9 +523,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",
"integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",
"cpu": [
"arm64"
],
@@ -540,9 +540,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",
"integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",
"cpu": [
"ia32"
],
@@ -557,9 +557,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",
"integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",
"cpu": [
"x64"
],
@@ -832,9 +832,9 @@
}
},
"node_modules/@pkgr/core": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
"integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -904,9 +904,9 @@
}
},
"node_modules/@types/node": {
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"version": "24.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1269,9 +1269,9 @@
}
},
"node_modules/esbuild": {
"version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1282,32 +1282,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.8",
"@esbuild/android-arm": "0.25.8",
"@esbuild/android-arm64": "0.25.8",
"@esbuild/android-x64": "0.25.8",
"@esbuild/darwin-arm64": "0.25.8",
"@esbuild/darwin-x64": "0.25.8",
"@esbuild/freebsd-arm64": "0.25.8",
"@esbuild/freebsd-x64": "0.25.8",
"@esbuild/linux-arm": "0.25.8",
"@esbuild/linux-arm64": "0.25.8",
"@esbuild/linux-ia32": "0.25.8",
"@esbuild/linux-loong64": "0.25.8",
"@esbuild/linux-mips64el": "0.25.8",
"@esbuild/linux-ppc64": "0.25.8",
"@esbuild/linux-riscv64": "0.25.8",
"@esbuild/linux-s390x": "0.25.8",
"@esbuild/linux-x64": "0.25.8",
"@esbuild/netbsd-arm64": "0.25.8",
"@esbuild/netbsd-x64": "0.25.8",
"@esbuild/openbsd-arm64": "0.25.8",
"@esbuild/openbsd-x64": "0.25.8",
"@esbuild/openharmony-arm64": "0.25.8",
"@esbuild/sunos-x64": "0.25.8",
"@esbuild/win32-arm64": "0.25.8",
"@esbuild/win32-ia32": "0.25.8",
"@esbuild/win32-x64": "0.25.8"
"@esbuild/aix-ppc64": "0.25.6",
"@esbuild/android-arm": "0.25.6",
"@esbuild/android-arm64": "0.25.6",
"@esbuild/android-x64": "0.25.6",
"@esbuild/darwin-arm64": "0.25.6",
"@esbuild/darwin-x64": "0.25.6",
"@esbuild/freebsd-arm64": "0.25.6",
"@esbuild/freebsd-x64": "0.25.6",
"@esbuild/linux-arm": "0.25.6",
"@esbuild/linux-arm64": "0.25.6",
"@esbuild/linux-ia32": "0.25.6",
"@esbuild/linux-loong64": "0.25.6",
"@esbuild/linux-mips64el": "0.25.6",
"@esbuild/linux-ppc64": "0.25.6",
"@esbuild/linux-riscv64": "0.25.6",
"@esbuild/linux-s390x": "0.25.6",
"@esbuild/linux-x64": "0.25.6",
"@esbuild/netbsd-arm64": "0.25.6",
"@esbuild/netbsd-x64": "0.25.6",
"@esbuild/openbsd-arm64": "0.25.6",
"@esbuild/openbsd-x64": "0.25.6",
"@esbuild/openharmony-arm64": "0.25.6",
"@esbuild/sunos-x64": "0.25.6",
"@esbuild/win32-arm64": "0.25.6",
"@esbuild/win32-ia32": "0.25.6",
"@esbuild/win32-x64": "0.25.6"
}
},
"node_modules/escape-string-regexp": {
@@ -2328,14 +2328,14 @@
}
},
"node_modules/prettier-plugin-packagejson": {
"version": "2.5.19",
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.19.tgz",
"integrity": "sha512-Qsqp4+jsZbKMpEGZB1UP1pxeAT8sCzne2IwnKkr+QhUe665EXUo3BAvTf1kAPCqyMv9kg3ZmO0+7eOni/C6Uag==",
"version": "2.5.18",
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.18.tgz",
"integrity": "sha512-NKznPGcGrcj4NPGxnh+w78JXPyfB6I4RQSCM0v+CAXwpDG7OEpJQ5zMyfC5NBgKH1k7Skwcj5ak5by2mrHvC5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"sort-package-json": "3.4.0",
"synckit": "0.11.11"
"synckit": "0.11.8"
},
"peerDependencies": {
"prettier": ">= 1.16.0"
@@ -2628,13 +2628,13 @@
}
},
"node_modules/synckit": {
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.9"
"@pkgr/core": "^0.2.4"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -2728,9 +2728,9 @@
}
},
"node_modules/typedoc-plugin-markdown": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.7.1.tgz",
"integrity": "sha512-HN/fHLm2S6MD4HX8txfB4eWvVBzX/mEYy5U5s1KTAdh3E5uX5/lilswqTzZlPTT6fNZInAboAdFGpbAuBKnE4A==",
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.7.0.tgz",
"integrity": "sha512-PitbnAps2vpcqK2gargKoiFXLWFttvwUbyns/E6zGIFG5Gz8ZQJGttHnYR9csOlcSjB/uyjd8tnoayrtsXG17w==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -237,9 +237,9 @@
}
},
"node_modules/@eslint/core": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
@@ -292,12 +292,12 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz",
"integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==",
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.15.1",
"@eslint/core": "^0.14.0",
"levn": "^0.4.1"
},
"engines": {
@@ -1822,6 +1822,18 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@eslint/core": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",

View File

@@ -360,9 +360,9 @@
"license": "MIT"
},
"node_modules/@pkgr/core": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
"integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -385,9 +385,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"version": "24.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1466,14 +1466,14 @@
}
},
"node_modules/prettier-plugin-packagejson": {
"version": "2.5.19",
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.19.tgz",
"integrity": "sha512-Qsqp4+jsZbKMpEGZB1UP1pxeAT8sCzne2IwnKkr+QhUe665EXUo3BAvTf1kAPCqyMv9kg3ZmO0+7eOni/C6Uag==",
"version": "2.5.18",
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.18.tgz",
"integrity": "sha512-NKznPGcGrcj4NPGxnh+w78JXPyfB6I4RQSCM0v+CAXwpDG7OEpJQ5zMyfC5NBgKH1k7Skwcj5ak5by2mrHvC5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"sort-package-json": "3.4.0",
"synckit": "0.11.11"
"synckit": "0.11.8"
},
"peerDependencies": {
"prettier": ">= 1.16.0"
@@ -1657,13 +1657,13 @@
}
},
"node_modules/synckit": {
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.9"
"@pkgr/core": "^0.2.4"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"

View File

@@ -3,7 +3,7 @@
from dataclasses import asdict
from time import sleep
from pyrad.client import Client, Timeout
from pyrad.client import Client
from pyrad.dictionary import Dictionary
from pyrad.packet import AccessAccept, AccessReject, AccessRequest
@@ -27,7 +27,7 @@ class TestProviderRadius(SeleniumTestCase):
"""Start radius container based on outpost created"""
self.run_container(
image=self.get_container_image("ghcr.io/goauthentik/dev-radius"),
ports={"1812/udp": 1812},
ports={"1812/udp": "1812/udp"},
environment={
"AUTHENTIK_TOKEN": outpost.token.key,
},
@@ -63,7 +63,7 @@ class TestProviderRadius(SeleniumTestCase):
sleep(5)
return outpost
@retry(exceptions=[Timeout])
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
@@ -85,7 +85,7 @@ class TestProviderRadius(SeleniumTestCase):
reply = srv.SendPacket(req)
self.assertEqual(reply.code, AccessAccept)
@retry(exceptions=[Timeout])
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",

777
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -97,7 +97,7 @@
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
"@lit/task": "^1.0.3",
"@lit/task": "^1.0.2",
"@mdx-js/mdx": "^3.1.0",
"@mrmarble/djangoql-completion": "^0.8.3",
"@open-wc/lit-helpers": "^0.7.0",
@@ -105,16 +105,16 @@
"@openlayers-elements/maps": "^0.4.0",
"@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.40.0",
"@sentry/browser": "^9.39.0",
"@spotlightjs/spotlight": "^3.0.1",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"change-case": "^5.4.4",
"chart.js": "^4.5.0",
"chart.js": "^4.4.9",
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.2",
"codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.44.0",
"core-js": "^3.42.0",
"country-flag-icons": "^1.5.19",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
@@ -156,13 +156,13 @@
"@storybook/addon-links": "^9.0.17",
"@storybook/web-components": "^9.0.17",
"@storybook/web-components-vite": "^9.0.17",
"@types/chart.js": "^4.0.1",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15",
"@types/dompurify": "^3.2.0",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.3",
"@types/mocha": "^10.0.10",
"@types/node": "^24.0.15",
"@types/node": "^24.0.14",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.8.0",
@@ -172,7 +172,7 @@
"@wdio/spec-reporter": "^9.15.0",
"@web/test-runner": "^0.20.2",
"chromedriver": "^136.0.3",
"esbuild": "^0.25.8",
"esbuild": "^0.25.6",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.1",
@@ -181,7 +181,7 @@
"eslint-plugin-wc": "^3.0.1",
"github-slugger": "^2.0.0",
"globals": "^15.10.0",
"knip": "^5.61.3",
"knip": "^5.58.0",
"lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",

View File

@@ -47,7 +47,7 @@
"devDependencies": {
"@goauthentik/prettier-config": "^1.0.5",
"@goauthentik/tsconfig": "^1.0.4",
"@types/node": "^24.0.15",
"@types/node": "^24.0.14",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
},

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@goauthentik/api": "^2024.6.0-1719577139",
"base64-js": "^1.5.1",
"bootstrap": "^5.3.7",
"bootstrap": "^4.6.1",
"formdata-polyfill": "^4.0.10",
"jquery": "^3.7.1",
"weakmap-polyfill": "^2.0.4"
@@ -23,7 +23,7 @@
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.7.8",
"@swc/core": "^1.13.1",
"@swc/core": "^1.12.14",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.5.3",
"rollup": "^4.45.1",

View File

@@ -59,7 +59,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
renderModal() {
let product = this.brandingTitle;
if (this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed) {
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`<div

View File

@@ -20,7 +20,7 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
renderStatusBanner() {
// Check if we're in the correct interface to render a banner
switch (this.licenseSummary?.status) {
switch (this.licenseSummary.status) {
// user warning is both on admin interface and user interface
case LicenseSummaryStatusEnum.LimitExceededUser:
if (
@@ -45,7 +45,7 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
break;
}
let message = "";
switch (this.licenseSummary?.status) {
switch (this.licenseSummary.status) {
case LicenseSummaryStatusEnum.LimitExceededAdmin:
case LicenseSummaryStatusEnum.LimitExceededUser:
message = msg(
@@ -83,12 +83,12 @@ export class EnterpriseStatusBanner extends WithLicenseSummary(AKElement) {
renderFlagBanner() {
return html`
${this.licenseSummary?.licenseFlags.includes(LicenseFlagsEnum.Trial)
${this.licenseSummary.licenseFlags.includes(LicenseFlagsEnum.Trial)
? html`<div class="pf-c-banner pf-m-sticky pf-m-gold">
${msg("This authentik instance uses a Trial license.")}
</div>`
: nothing}
${this.licenseSummary?.licenseFlags.includes(LicenseFlagsEnum.NonProduction)
${this.licenseSummary.licenseFlags.includes(LicenseFlagsEnum.NonProduction)
? html`<div class="pf-c-banner pf-m-sticky pf-m-gold">
${msg("This authentik instance uses a Non-production license.")}
</div>`

View File

@@ -17,7 +17,7 @@ export interface LicenseMixin {
/**
* Summary of the current license.
*/
readonly licenseSummary: LicenseSummary | null;
readonly licenseSummary: LicenseSummary;
/**
* Whether or not the current license is an enterprise license.
@@ -39,12 +39,12 @@ export const WithLicenseSummary = createMixin<LicenseMixin>(
context: LicenseContext,
subscribe,
})
public readonly licenseSummary: LicenseSummary | null = null;
public readonly licenseSummary!: LicenseSummary;
get hasEnterpriseLicense() {
return (
this.licenseSummary?.status === LicenseSummaryStatusEnum.Valid ||
this.licenseSummary?.status === LicenseSummaryStatusEnum.ExpirySoon
this.licenseSummary.status === LicenseSummaryStatusEnum.Valid ||
this.licenseSummary.status === LicenseSummaryStatusEnum.ExpirySoon
);
}
}

View File

@@ -48,12 +48,12 @@
"@rspack/binding-darwin-arm64": "1.4.8",
"@rspack/binding-linux-arm64-gnu": "1.4.8",
"@rspack/binding-linux-x64-gnu": "1.4.8",
"@swc/core-darwin-arm64": "1.13.0",
"@swc/core-linux-arm64-gnu": "1.13.0",
"@swc/core-linux-x64-gnu": "1.13.0",
"@swc/html-darwin-arm64": "1.13.0",
"@swc/html-linux-arm64-gnu": "1.13.0",
"@swc/html-linux-x64-gnu": "1.13.0",
"@swc/core-darwin-arm64": "1.12.14",
"@swc/core-linux-arm64-gnu": "1.12.14",
"@swc/core-linux-x64-gnu": "1.12.14",
"@swc/html-darwin-arm64": "1.12.14",
"@swc/html-linux-arm64-gnu": "1.12.14",
"@swc/html-linux-x64-gnu": "1.12.14",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"

View File

@@ -4,81 +4,60 @@ title: Writing documentation
Writing documentation for authentik is a great way for both new and experienced users to improve and contribute to the project. We appreciate contributions to our documentation; everything from fixing a typo to adding additional content to writing a completely new topic.
The technical documentation (https://docs.goauthentik.io/docs/) and our integration guides (https://integrations.goauthentik.io/) are built, formatted, and tested using npm. The commands to build the content locally are defined in the `Makefile` in the root of the repository. Each command is prefixed with `docs-` or `integrations-` and corresponds to an NPM script within the `website` directory.
## Requirements
- Node.js 24 (or later)
## Guidelines
Adhering to the following guidelines will help us get your PRs merged much easier and faster, with fewer edits needed.
- Ideally, when you are making contributions to the documentation, you should fork and clone our repo, then [build it locally](#set-up-your-local-build-tools), so that you can test the docs and run the required linting and spell checkers before pushing your PR. While you can do much of the writing and editing within the GitHub UI, you cannot run the required linters from the GitHub UI.
- After submitting a PR, you can view the Netlify Deploy Preview for the PR on GitHub, to check that your content rendered correctly, links work, etc. This is especially useful when using Docusaurus-specific features in your content.
- Ideally, when you are making contributions to the documentation, you should fork and clone our repo, then [build it locally](#set-up-your-local-build), so that you can test the docs and run the required linting and spell checkers before pushing your PR. While you can do much of the writing and editing within the GitHub UI, you cannot run the required linters from the GitHub UI.
- Please refer to our [Style Guide](./style-guide.mdx) for authentik documentation. Here you will learn important guidelines about not capitalizing authentik, how we format our titles and headers, and much more.
- Remember to use our templates when possible; they are already set up to follow our style guidelines, they make it a lot easier for you (no blank page frights!), and they keep the documentation structure and headings consistent.
- [docs templates](./templates/index.md)
- [integration guide template](https://integrations.goauthentik.io/applications#add-a-new-application)
- Remember to use our [docs templates](./templates/index.md) when possible; they are already set up to follow our style guidelines, they make it a lot easier for you (no blank page frights!), and keeps the documentation structure and headings consistent.
:::tip
If you encounter build check fails, or issues you with your local build, you might need to run `make docs-install` in order to get the latest build tools and dependencies; we do occasionally update our build tools.
:::
- To test how the documentation renders you can build locally and then use the Netlify Deploy Preview, especially when using Docusaurus-specific features. You can also run the `make docs-watch` command on your local build, to see the rendered pages as you make changes.
## Clone and fork the authentik repository
- Be sure to run the `make docs` command on your local branch, before pushing the PR to the authentik repo. This command does important linting, and the build check in our repo will fail if the linting has not been done.
The documentation, integration guides, API docs, and the code are in the same [GitHub repo](https://github.com/goauthentik/authentik), so if you have cloned and forked the repo, you already have the docs and integration guides.
- For new entries, make sure to add any new pages to the appropriate `sidebar.js` file. Otherwise, the new page will not appear in the table of contents to the left.
## Set up your local build tools
## Set up your local build
Run the following command to install the build tools for both the technical docs and integration guides.
Requirements:
- **Install (or update) the build tools**: `make docs-install`
- Node.js 20 (or greater, we use Node.js 24)
Installs the build dependencies such as Docusaurus, Prettier, and ESLint. You should run this command when you are first setting up your writing environment, and also if you encounter build check fails either when you build locally or when you push your PR to the authentik repository. Running this command will grab any new dependencies that we might have added to our build tool package.
The docs and the code are in the same Github repo, at https://github.com/goauthentik/authentik, so if you have cloned the repo, you already have the docs.
## Writing or modifying technical docs
You can do local builds of the documentation to test your changes or review your new content, and to run the required `make docs` command (which runs `prettier` and other linters) before pushing your PR.
In addition to following the [Style Guide](./style-guide.mdx) please review the following guidelines about our technical documentation (https://docs.goauthentik.io/docs/):
The documentation site is situated in the `/website` folder of the repo.
- For new entries, make sure to add any new pages to the `/docs/sidebar.mjs` file.
Otherwise, the new page will not appear in the table of contents to the left.
The site is built using npm, below are some useful make commands:
- Always be sure to run the `make docs` command on your local branch _before_ pushing the PR to the authentik repo. This command does important linting, and the build check in our repo will fail if the linting has not been done. In general, check on the health of your build before pushing to the authentik repo, and also check on the build status of your PR after you create it.
- **Installation**: `make docs-install`
For our technical documentation (https://docs.goauthentik.io/docs/), the following commands are used:
This command is required before running any of the following commands, and after upgrading any dependencies.
- **Build locally**: `make docs`
- **Formatting**: `make docs`, `make docs-lint-fix`, or `npm run prettier`
This command is a combination of `make docs-lint-fix` and `make docs-build`. It is important to run this command before committing changes because linter errors will prevent the build checks from passing.
Run the appropriate formatting command for your set up before committing, to ensure consistent syntax, clean formatting, and verify links. Note that if the formatting command is not run, the build will fail with an error about linting.
- **Live editing**: `make docs-watch`
Starts a development server for the documentation site. This command will automatically rebuild your local documentation site whenever you make changes to the Markdown files in the `website/docs` directory.
For real-time viewing of changes, as you make them.
## Writing or modifying integration guides
:::info
Be sure to run a formatting command before committing changes.
:::
In addition to following the [Style Guide](./style-guide.mdx) please review the following guidelines about our integration guides (https://integrations.goauthentik.io/).
## Documentation for integrations
- For new integration documentation, please use the Integrations template in our [Github repo](https://github.com/goauthentik/authentik) at `/website/integrations/template/service.md`.
In addition to following the [Style Guide](./style-guide.mdx) please review the following guidelines.
For new integration documentation, please use the Integrations template in our [Github repo](https://github.com/goauthentik/authentik) at `/website/integrations/template/service.md`.
- For placeholder domains, use `authentik.company` and `app-name.company`, where `app-name` is the name of the application that you are writing documentation for.
- Make sure to create a directory for your service in a fitting category within [`/website/integrations/`](https://github.com/goauthentik/authentik/tree/main/website/integrations).
Make sure to create a directory for your service in a fitting category within `/website/integrations/`.
:::tip Sidebars and categories
You no longer need to modify the integrations sidebar file manually. This is now automatically generated from the categories in [`/website/integrations/categories.mjs`](https://github.com/goauthentik/authentik/blob/main/website/integrations/categories.mjs).
:::tip
You no longer need to modify the integrations sidebar file manually. This is now automatically generated from the categories in `/website/integrations/categories.mjs`.
:::
When authoring integration guides, the following commands are used:
- **Build locally**: `make integrations`
This command is a combination of `make docs-lint-fix` and `make integrations-build`. This command should always be run on your local branch before committing your changes to a pull request to the authentik repo. It is important to run this command before committing changes because linter errors will prevent the build checks from passing.
- **Live editing**: `make integrations-watch`
Starts a development server for the integrations guides. This command will automatically rebuild your local integrations site whenever you make changes to the Markdown files in the [`/website/integrations/`](https://github.com/goauthentik/authentik/tree/main/website/integrations) directory.

View File

@@ -26,8 +26,8 @@ import Tabs from "@theme/Tabs";
## Services Setup
For PostgreSQL and Redis, you can use the `docker-compose.yml` file in `/scripts`. To use these pre-configured database instances, navigate to the `/scripts` directory in your local copy of the authentik git repo, and start the services by running `docker compose up -d`.
Alternatively, you can also use a native install, if you prefer.
For PostgreSQL and Redis, you can use the `docker-compose.yml` file in `/scripts`.To use these pre-configured database instances, navigate to the `/scripts` directory in your local copy of the authentik git repo, and run `docker compose up -d`.
You can also use a native install, if you prefer.
:::info
If you use locally installed databases, the PostgreSQL credentials given to authentik should have permissions for `CREATE DATABASE` and `DROP DATABASE`, because authentik creates a temporary database for tests.
@@ -147,18 +147,6 @@ To define a password for the default admin (called **akadmin**), you can manuall
In case of issues in this process, feel free to use `make dev-reset` which drops and restores the authentik PostgreSQL instance to a "fresh install" state.
:::
## End-to-End (E2E) Setup
To run E2E tests, navigate to the `/tests/e2e` directory in your local copy of the authentik git repo, and start the services by running `docker compose up -d`.
You can then view the Selenium Chrome browser via http://localhost:7900/ using the password: `secret`.
Alternatively, you can connect directly via VNC on port `5900` using the password: `secret`.
:::note
When using Docker Desktop, host networking needs to be enabled via **Docker Settings** > **Resources** > **Network** > **Enable host networking**.
:::
## Submitting Pull Requests
Before submitting a pull request, run the following commands in the same directory as your local authentik git repository:

View File

@@ -450,7 +450,6 @@ const items = [
"users-sources/user/user_basic_operations",
"users-sources/user/user_ref",
"users-sources/user/invitations",
"users-sources/user/password_reset_on_login",
],
},
{

View File

@@ -1,172 +0,0 @@
---
title: Notification Rule Expression Policies
---
## Introduction
Notification rules with bound expression policies are very powerful. The following are examples of what can be achieved.
### Change user attributes upon account deactivation
This example code is triggered when a user account with the `sshPublicKey` attribute set is deactivated. It saves the `sshPublicKey` attribute to a new `inactivesshPublicKey` attribute, and subsequently nullifies the `sshPublicKey` attribute.
```python
from authentik.core.models import User
# Check if an event has occurred
event = request.context.get("event", None)
if not event:
ak_logger.info("no event")
return False
# Check if the event action includes updating a model
if event.action != "model_updated":
ak_logger.info("event action does not match")
return False
model_app = event.context["model"]["app"]
model_name = event.context["model"]["model_name"]
# Check if the model that was updated is the user model
if model_app != "authentik_core" or model_name != "user":
ak_logger.info("model does not match")
user_pk = event.context["model"]["pk"]
user = User.objects.filter(pk=user_pk).first()
# Check if an user object was found
if not user:
ak_logger.info("user not found")
return False
# Check if user is active
if user.is_active:
ak_logger.info("user is active, not changing")
return False
# Check if user has the `sshPublicKey` attribute set
if not user.attributes.get("sshPublicKey"):
ak_logger.info("no public keys to remove")
return False
# Save the `sshPublicKey` attribute to a new `inactiveSSHPublicKey` attribute
user.attributes["inactiveSSHPublicKey"] = user.attributes["sshPublicKey"]
# Nullify the `sshPublicKey` attribute
user.attributes["sshPublicKey"] = []
# Save the changes made to the user
user.save()
return False
```
### Alert when application is created without binding
This code is triggered when a new application is created without any user, group, or policy bound to it. The notification rule can then be configured to alert an administrator. This feature is useful for ensuring limited access to applications, as by default, an application without any users, groups, or policies bound to it can be accessed by all users.
```python
from authentik.core.models import Application
from authentik.policies.models import PolicyBinding
# Check if an event has occurred
event = request.context.get("event", None)
if not event:
ak_logger.info("no event")
return False
# Check if the event action includes creating a model
if event.action != "model_created":
ak_logger.info("event action does not match")
return False
model_app = event.context["model"]["app"]
model_name = event.context["model"]["model_name"]
# Check if the model that was created is the application model
if model_app != "authentik_core" or model_name != "application":
ak_logger.info("model does not match")
application_pk = event.context["model"]["pk"]
application = Application.objects.filter(pk=application_pk).first()
# Check if an application object was found
if not application:
ak_logger.info("application not found")
return False
# Check if application has binding
if PolicyBinding.objects.filter(target=application).exists():
output = PolicyBinding.objects.filter(target=application)
ak_logger.info("application has bindings, returning true")
return True
return False
```
### Append user addition history to group attributes
This code is triggered when a user is added to a group. It then creates and updates a `UserAddedHistory` attribute to the group with a date/time stamp and the username of the added user. This functionality is already available within the changelog of a group, but this code can be used as a template to trigger alerts or other events.
:::note
This policy interacts with the `diff` event output. This field is only available with an enterprise license.
:::
```python
from authentik.core.models import User
from authentik.core.models import Group
from datetime import datetime
# Check if an event has occurred
event = request.context.get("event", None)
if not event:
ak_logger.info("no event")
return False
# Check if the event action includes updating a model
if event.action != "model_updated":
ak_logger.info("event action does not match")
return False
model_app = event.context["model"]["app"]
model_name = event.context["model"]["model_name"]
# Check if the model that was updated is the group model
if model_app != "authentik_core" or model_name != "group":
ak_logger.info("model does not match")
group_pk = event.context["model"]["pk"]
group = Group.objects.filter(pk=group_pk).first()
# If user was added to group, get user object, else return false
if "add" in event.context["diff"]["users"]:
ak_logger.info("user added to group")
user_pk = event.context["diff"]["users"]["add"][0]
user = User.objects.filter(pk=user_pk).first()
else:
ak_logger.info("user not added to group")
return False
# Check if a group object was found
if not group:
ak_logger.info("group not found")
return False
# Check if an user object was found
if not user:
ak_logger.info("user not found")
return False
if not group.attributes.get("UserAddedHistory"):
group.attributes["UserAddedHistory"] = []
current_date_time = datetime.now().isoformat(timespec='seconds')
group.attributes["UserAddedHistory"].append(current_date_time + " - Added user: " + user.username)
# Save the changes made to the group
group.save()
return False
```

View File

@@ -27,13 +27,13 @@ You will need to create a policy (either the **Event Matcher** policy or a custo
### Event Matcher policy
For simple event matching you can [create and configure](../../customize/policies/working_with_policies.md) an **Event Matcher policy** to define which events (known as _Actions_ in the policy) will trigger a notification. For example, whenever a user deletes a model object, or whenever any user fails to successfully log in.
For simple event matching you can [create and configure](../../customize/policies/working_with_policies.md) a **Event Matcher** policy to define which events (known as _Actions_ in the policy) will trigger a notification. For example, whenever a user deletes a model object, or whenever any user fails to successfully log in.
Be aware that an event has to match all configured fields in the policy, otherwise the notification rule will not trigger.
### Expression policy for events
To match events with an **Expression Policy**, you can write code like so:
To match events with an "Expression Policy", you can write code like so:
```python
if "event" not in request.context:
@@ -42,8 +42,6 @@ if "event" not in request.context:
return ip_address(request.context["event"].client_ip) in ip_network('192.0.2.0/24')
```
For more code examples, see [notification rule expression policies](./notification_rule_expression_policies.mdx).
## 3. Create a notification rule and bind it to the policy
After you've created the policies to match the events you want, create a notification rule.

View File

@@ -1,112 +0,0 @@
---
title: Force password reset on next login
sidebar_label: Password reset on login
---
You can require users to reset their password on their next login, using expression policies, custom stages, and a custom user attribute. This guide explains how to configure this with the `default-authentication-flow`; however, the same steps apply to any authentication flow.
Configuring forced password reset on next login involves the following steps:
1. Creating two expression policicies.
2. Creating and binding two stages to the active authentication flow.
3. Binding the expression policies to the stages.
4. Setting a custom user attribute which triggers the password prompt.
## Create expression policies
You'll need to create two expression policies; one that checks the value of a custom user attribute on the user account attempting to log in, and another that resets the value of the custom user attribute.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Customization** > **Policies** and click **Create** to set up the first policy.
3. Select **Expression Policy** as the policy type, click **Next**, and configure the following settings:
- **Name**: Provide a descriptive name for the policy (e.g. `reset_password_check`).
- **Expression**:
```python
# Check if the "reset_password" attribute set to true for the pending user
if request.context["pending_user"].attributes.get("reset_password") == True:
return True
return False
```
4. Click **Finish** to save the first policy, then repeat the steps to create the second policy using the following settings:
- **Name**: Provide a descriptive name for the policy (e.g. `reset_password_update`).
- **Expression**:
```python
# Check if the "reset_password" attribute is set to true for the pending user
if request.context["pending_user"].attributes.get("reset_password") == True:
# Reset the "reset_password" attribute to false to prevent forcing a password reset on next login
request.context["pending_user"].attributes["reset_password"] = False
return True
return False
```
5. Click **Finish**.
## Create stages
You'll need to create two stages; a _Prompt stage_ to prompt the user to enter a new password, and a _User Write stage_ to update the user's account with the new password. Both stages will need to be bound to the active authentication flow, typically the `default-authentication-flow`.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Flows and Stages** > **Flows** and click on the name of the active authentication flow, typically the `default-authentication-flow`.
3. Select the **Stage Bindings** tab and click **Create and bind stage**.
4. Select **Prompt Stage** as the stage type, click **Next**, and configure the following settings for the stage:
- **Name**: Provide a descriptive name for the stage (e.g. `Force Password Reset Prompt Stage`).
- Under **Fields**:
- Click the `x` icon between **Available Fields** and **Selected Fields** to clear the selections.
- Select `default-password-change-field-password` and `default-password-change-field-password-repeat`.
- Under **Validation Policies**:
- Click the `x` icon between **Available Policies** and **Selected Policies** to clear the selections.
- _(Optional but recommended)_ Select `default-password-change-policy`.
:::tip
Optionally, you can create and add a text field to the prompt stage to inform users that they are required to reset their password. For more details on configuring this, refer to the [Prompt Stage documentation](../../add-secure-apps/flows-stages/stages/prompt/index.md).
:::
5. Click **Next** to create the stage and then configure the following settings for the binding:
- **Order**: `25` or any number higher than the `default-authentication-password` stage order and lower than the `default-authentication-mfa-validation` stage order.
- Leave the other settings as their default values.
6. Click **Finish** to create the binding and repeat the process for the second stage using the following settings:
- **Stage type**: Select **User Write Stage** as the type.
- **Name**: Provide a descriptive name for the stage (e.g. `Force Password Reset User Write Stage`).
- Leave the other settings as their default values.
7. Click **Next** to create the stage and then configure the following settings for the binding:
- **Order**: `26` or any number higher than the `Force Password Reset Prompt Stage` stage order and lower than the `default-authentication-mfa-validation` stage order.
- Leave the other settings as their default values.
8. Click **Finish** to create the binding.
## Bind policies to stages
You will need to bind the previously created policies to the newly created stages. Specifically:
- The `reset_password_check` policy needs to be bound to the `Force Password Reset Prompt Stage`.
- The `reset_password_update` policy needs to be bound to the `Force Password Reset User Write Stage`.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Flows and Stages** > **Flows** and click on the name of the active authentication flow, typically `default-authentication-flow`.
3. Select the **Stage Bindings** tab and click the arrow next to the newly created `Force Password Reset Prompt Stage` to expand it.
4. Click **Bind existing Policy / Group / User**.
5. Set **Policy** to `reset_password_check` and click **Create**.
6. Click the arrow next to the newly created `Force Password Reset User Write Stage` to expand it.
7. Click **Bind existing Policy / Group / User**.
8. Set **Policy** to `reset_password_update` and click **Create**.
## Set custom user attribute
To require a user to reset their password on next login, you will need to set a custom user attribute on their account.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Directory** > **Users** and click the **Edit** icon of the user in question.
3. Add the following values to the user's attribute field:
```python
reset_password: True
```
4. Click **Update**.
The next time the user logs in, they will be required to reset their password, and the `reset_password` attribute on their account will be set to `False`.

View File

@@ -16,7 +16,7 @@ The User object has the following properties:
- `password_change_date`: Date password was last changed. Read-only.
- `path`: User's path, see [Path](#path)
- `attributes`: Dynamic attributes, see [Attributes](#attributes)
- `group_attributes()`: Merged attributes of all groups the user is a member of and the user's own attributes. Read-only.
- `group_attributes()`: Merged attributes of all groups the user is member of and the user's own attributes. Ready-only.
- `ak_groups`: This is a queryset of all the user's groups.
## Examples

View File

@@ -13,7 +13,7 @@ support_level: community
> -- https://nodered.org/
:::caution
This requires modification of the Node-RED `settings.js` file and installing additional Passport-js packages; see [Securing Node-RED](https://nodered.org/docs/user-guide/runtime/securing-node-red#oauthopenid-based-authentication) documentation for further details.
This requires modification of the Node-RED settings.js and installing additional Passport-js packages, see [Securing Node-RED](https://nodered.org/docs/user-guide/runtime/securing-node-red#oauthopenid-based-authentication) documentation for further details.
:::
## Preparation
@@ -66,29 +66,29 @@ Edit the node-red settings.js file `/data/settings.js` to use the external authe
```js
adminAuth: {
type:"strategy",
strategy: {
name: "openidconnect",
label: 'Sign in with authentik',
icon:"fa-cloud",
strategy: require("passport-openidconnect").Strategy,
options: {
issuer: 'https://authentik.company/application/o/<application_slug>/',
authorizationURL: 'https://authentik.company/application/o/authorize/',
tokenURL: 'https://authentik.company/application/o/token/',
userInfoURL: 'https://authentik.company/application/o/userinfo/',
clientID: '<Client ID (Key): Step 2>',
clientSecret: '<Client Secret: Step 2>',
callbackURL: 'https://nodered.company/auth/strategy/callback/',
scope: ['email', 'profile', 'openid'],
proxy: true,
verify: function(context, issuer, profile, done) {
return done(null, profile);
},
}
},
users: function(user) {
return Promise.resolve({ username: user, permissions: "*" });
type:"strategy",
strategy: {
name: "openidconnect",
label: 'Sign in with authentik',
icon:"fa-cloud",
strategy: require("passport-openidconnect").Strategy,
options: {
issuer: 'https://authentik.company/application/o/<application_slug>/',
authorizationURL: 'https://authentik.company/application/o/authorize/',
tokenURL: 'https://authentik.company/application/o/token/',
userInfoURL: 'https://authentik.company/application/o/userinfo/',
clientID: '<Client ID (Key): Step 2>',
clientSecret: '<Client Secret: Step 2>',
callbackURL: 'https://nodered.company/auth/strategy/callback/',
scope: ['email', 'profile', 'openid'],
proxy: true,
verify: function(issuer, profile, done) {
done(null, profile)
}
}
},
users: function(user) {
return Promise.resolve({ username: user, permissions: "*" });
}
},
```

View File

@@ -140,6 +140,54 @@ This section depends on the operating system hosting Apache Guacamole.
More information on the keytool command can be found in the [Oracle documentation.](https://docs.oracle.com/en/java/javase/21/docs/specs/man/keytool.html)
:::
### Self Signed Certificates
When using a self-signed certificate, it is necessary to incorporate the certificate of the corresponding Certificate Authority into both the `/etc/ssl/certs/ca-certificates.crt` file and the `/opt/java/openjkd/jre/lib/security/cacerts` keystore on your Apache Guacamole host. This ensures that the self-signed certificate is trusted by both the system and the Java runtime environment used by Guacamole.
#### Adding Certificate Authority certificate as trusted in `/etc/ssl/certs/ca-certificates.crt`
:::note
This section depends on the operating system hosting Apache Guacamole.
:::
##### For _Debian_ based operating systems:
1. Copy the certificate of the Certificate Authority (e.g. `<CA_certificate>.crt`) to the `/usr/local/share/ca-certificates/` directory on the Apache Guacamole host. Ensure that the file extension is `.crt`.
2. To add the certificate as trusted in `/etc/ssl/certs/ca-certificates.crt`, use the following command:
```shell
update-ca-certificates
```
##### For _Synology_ systems:
1. Copy the certificate of the Certificate Authority (e.g. `<CA_certificate>.crt`) to the `/usr/syno/etc/security-profile/ca-bundle-profile/ca-certificates/` directory on the Synology host. Ensure that the filetype is `.crt`.
2. To add the certificate as trusted in `/etc/ssl/certs/ca-certificates.crt`, use the following command:
```shell
update-ca-certificates.sh
```
#### Adding Certificate Authority certificate to `/opt/java/openjkd/jre/lib/security/cacerts`
1. To export the certificate of the Certificate Authority, use the following command on the Certificate Authority host:
```shell
openssl pkcs12 -export -in <CA_certificate>.crt -inkey <CA_certificate>.key -out <CA_certificate>.p12 -passout pass:<password>
```
2. To import the certificate to the `/opt/java/openjdk/jre/lib/security/cacerts` keystore on the Apache Guacamole host, use the following command:
```shell
keytool -importkeystore -srckeystore <CA_certificate>.p12 -srcstoretype PKCS12 -keystore /opt/java/openjdk/jre/lib/security/cacerts -deststorepass <destination_store_password> -nopromt -srcstorepass <password>
```
:::note
More information on the keytool command can be found in the [Oracle documentation.](https://docs.oracle.com/en/java/javase/21/docs/specs/man/keytool.html)
:::
## Configuration verification
To verify that authentik is correctly configured with Apache Guacamole, log out and log back in through authentik. You should notice a new button appearing at the bottom left of the login page.

View File

@@ -19,13 +19,13 @@
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/prettier-config": "^3.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@types/node": "^24.0.15",
"@types/node": "^24.0.14",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"eslint": "^9.30.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.6.2",
"prettier-plugin-packagejson": "^2.5.19",
"prettier-plugin-packagejson": "^2.5.18",
"typescript-eslint": "^8.37.0"
},
"engines": {
@@ -35,12 +35,12 @@
"@rspack/binding-darwin-arm64": "1.4.8",
"@rspack/binding-linux-arm64-gnu": "1.4.8",
"@rspack/binding-linux-x64-gnu": "1.4.8",
"@swc/core-darwin-arm64": "1.13.0",
"@swc/core-linux-arm64-gnu": "1.13.0",
"@swc/core-linux-x64-gnu": "1.13.0",
"@swc/html-darwin-arm64": "1.13.0",
"@swc/html-linux-arm64-gnu": "1.13.0",
"@swc/html-linux-x64-gnu": "1.13.0",
"@swc/core-darwin-arm64": "1.12.14",
"@swc/core-linux-arm64-gnu": "1.12.14",
"@swc/core-linux-x64-gnu": "1.12.14",
"@swc/html-darwin-arm64": "1.12.14",
"@swc/html-linux-arm64-gnu": "1.12.14",
"@swc/html-linux-x64-gnu": "1.12.14",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"
@@ -79,12 +79,12 @@
"@rspack/binding-darwin-arm64": "1.4.8",
"@rspack/binding-linux-arm64-gnu": "1.4.8",
"@rspack/binding-linux-x64-gnu": "1.4.8",
"@swc/core-darwin-arm64": "1.13.0",
"@swc/core-linux-arm64-gnu": "1.13.0",
"@swc/core-linux-x64-gnu": "1.13.0",
"@swc/html-darwin-arm64": "1.13.0",
"@swc/html-linux-arm64-gnu": "1.13.0",
"@swc/html-linux-x64-gnu": "1.13.0",
"@swc/core-darwin-arm64": "1.12.14",
"@swc/core-linux-arm64-gnu": "1.12.14",
"@swc/core-linux-x64-gnu": "1.12.14",
"@swc/html-darwin-arm64": "1.12.14",
"@swc/html-linux-arm64-gnu": "1.12.14",
"@swc/html-linux-x64-gnu": "1.12.14",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"
@@ -6584,9 +6584,9 @@
}
},
"node_modules/@pkgr/core": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
"integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
@@ -7269,9 +7269,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.0.tgz",
"integrity": "sha512-SkmR9u7MHDu2X8hf7SjZTmsAfQTmel0mi+TJ7AGtufLwGySv6pwQfJ/CIJpcPxYENVqDJAFnDrHaKV8mgA6kxQ==",
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.14.tgz",
"integrity": "sha512-HNukQoOKgMsHSETj8vgGGKK3SEcH7Cz6k4bpntCxBKNkO3sH7RcBTDulWGGHJfZaDNix7Rw2ExUVWtLZlzkzXg==",
"cpu": [
"arm64"
],
@@ -7317,9 +7317,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.0.tgz",
"integrity": "sha512-qyZmBZF7asF6954/x7yn6R7Bzd45KRG05rK2atIF9J3MTa8az7vubP1Q3BWmmss1j8699DELpbuoJucGuhsNXw==",
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.14.tgz",
"integrity": "sha512-akUAe1YrBqZf1EDdUxahQ8QZnJi8Ts6Ya0jf6GBIMvnXL4Y6QIuvKTRwfNxy7rJ+x9zpzP1Vlh14ZZkSKZ1EGA==",
"cpu": [
"arm64"
],
@@ -7349,9 +7349,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.0.tgz",
"integrity": "sha512-51n4P4nv6rblXyH3zCEktvmR9uSAZ7+zbfeby0sxbj8LS/IKuVd7iCwD5dwMj4CxG9Fs+HgjN73dLQF/OerHhg==",
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.14.tgz",
"integrity": "sha512-71EPPccwJiJUxd2aMwNlTfom2mqWEWYGdbeTju01tzSHsEuD7E6ePlgC3P3ngBqB3urj41qKs87z7zPOswT5Iw==",
"cpu": [
"x64"
],
@@ -7428,54 +7428,6 @@
"node": ">=10"
}
},
"node_modules/@swc/core/node_modules/@swc/core-darwin-arm64": {
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.14.tgz",
"integrity": "sha512-HNukQoOKgMsHSETj8vgGGKK3SEcH7Cz6k4bpntCxBKNkO3sH7RcBTDulWGGHJfZaDNix7Rw2ExUVWtLZlzkzXg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core/node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.14.tgz",
"integrity": "sha512-akUAe1YrBqZf1EDdUxahQ8QZnJi8Ts6Ya0jf6GBIMvnXL4Y6QIuvKTRwfNxy7rJ+x9zpzP1Vlh14ZZkSKZ1EGA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core/node_modules/@swc/core-linux-x64-gnu": {
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.14.tgz",
"integrity": "sha512-71EPPccwJiJUxd2aMwNlTfom2mqWEWYGdbeTju01tzSHsEuD7E6ePlgC3P3ngBqB3urj41qKs87z7zPOswT5Iw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -7507,9 +7459,9 @@
}
},
"node_modules/@swc/html-darwin-arm64": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.13.0.tgz",
"integrity": "sha512-tRAScqaFehYDugB3F+eWiEvsWpL+2broMrt7amY6lN6Eemu2VSL91KykvaiaBXhmhJFL9Z2kFHgV3jD4lCTUcA==",
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.14.tgz",
"integrity": "sha512-qbZBSd2oalqBxGfVwpgT9I7gaCleL05XjuatMZIca94QhvLp0+FhCNqujrdq6Ggcrlk8IVoxtsVS5GTeDH5jAw==",
"cpu": [
"arm64"
],
@@ -7555,9 +7507,9 @@
}
},
"node_modules/@swc/html-linux-arm64-gnu": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.13.0.tgz",
"integrity": "sha512-ppau7AB47WasAsTKjUlLdK40hpAPKkFMoaY2IgDhM5Qdi4H84sPf5z7AcXupMnTEm4QmFM2qZBXGNnxIp1W0sg==",
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.14.tgz",
"integrity": "sha512-wjUyMqxYSp/NGEXwdyEVfE/P/1DNt090KGEM/UbH55i1oyIYqq4T+FJacn7ITtypJp4lYQ7s7Bb5bB/BX+TZ1w==",
"cpu": [
"arm64"
],
@@ -7587,9 +7539,9 @@
}
},
"node_modules/@swc/html-linux-x64-gnu": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.13.0.tgz",
"integrity": "sha512-9UNYdUt4n/LG2VxLNtCXkEjZNPj2yQuo6Z/F93+12qlueG2GUVSDuN1d/M/WogZq1uthRbwjF2QQbsrTVhXOHA==",
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.14.tgz",
"integrity": "sha512-oLqGIB9u7X+SI0Zu2kxAOgAzEOr53n030esUcXzwGRn0BzCxAF2NaQav9HwKrCboQfK3SDtVGx/7Hy7metgacA==",
"cpu": [
"x64"
],
@@ -7666,54 +7618,6 @@
"node": ">=10"
}
},
"node_modules/@swc/html/node_modules/@swc/html-darwin-arm64": {
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.14.tgz",
"integrity": "sha512-qbZBSd2oalqBxGfVwpgT9I7gaCleL05XjuatMZIca94QhvLp0+FhCNqujrdq6Ggcrlk8IVoxtsVS5GTeDH5jAw==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/html/node_modules/@swc/html-linux-arm64-gnu": {
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.14.tgz",
"integrity": "sha512-wjUyMqxYSp/NGEXwdyEVfE/P/1DNt090KGEM/UbH55i1oyIYqq4T+FJacn7ITtypJp4lYQ7s7Bb5bB/BX+TZ1w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/html/node_modules/@swc/html-linux-x64-gnu": {
"version": "1.12.14",
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.14.tgz",
"integrity": "sha512-oLqGIB9u7X+SI0Zu2kxAOgAzEOr53n030esUcXzwGRn0BzCxAF2NaQav9HwKrCboQfK3SDtVGx/7Hy7metgacA==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/types": {
"version": "0.1.23",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz",
@@ -8254,9 +8158,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"version": "24.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
@@ -23859,13 +23763,13 @@
}
},
"node_modules/prettier-plugin-packagejson": {
"version": "2.5.19",
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.19.tgz",
"integrity": "sha512-Qsqp4+jsZbKMpEGZB1UP1pxeAT8sCzne2IwnKkr+QhUe665EXUo3BAvTf1kAPCqyMv9kg3ZmO0+7eOni/C6Uag==",
"version": "2.5.18",
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.18.tgz",
"integrity": "sha512-NKznPGcGrcj4NPGxnh+w78JXPyfB6I4RQSCM0v+CAXwpDG7OEpJQ5zMyfC5NBgKH1k7Skwcj5ak5by2mrHvC5g==",
"license": "MIT",
"dependencies": {
"sort-package-json": "3.4.0",
"synckit": "0.11.11"
"synckit": "0.11.8"
},
"peerDependencies": {
"prettier": ">= 1.16.0"
@@ -27565,12 +27469,12 @@
}
},
"node_modules/synckit": {
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
"integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.9"
"@pkgr/core": "^0.2.4"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"

View File

@@ -24,25 +24,25 @@
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/prettier-config": "^3.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@types/node": "^24.0.15",
"@types/node": "^24.0.14",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"eslint": "^9.30.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.6.2",
"prettier-plugin-packagejson": "^2.5.19",
"prettier-plugin-packagejson": "^2.5.18",
"typescript-eslint": "^8.37.0"
},
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "1.4.8",
"@rspack/binding-linux-arm64-gnu": "1.4.8",
"@rspack/binding-linux-x64-gnu": "1.4.8",
"@swc/core-darwin-arm64": "1.13.0",
"@swc/core-linux-arm64-gnu": "1.13.0",
"@swc/core-linux-x64-gnu": "1.13.0",
"@swc/html-darwin-arm64": "1.13.0",
"@swc/html-linux-arm64-gnu": "1.13.0",
"@swc/html-linux-x64-gnu": "1.13.0",
"@swc/core-darwin-arm64": "1.12.14",
"@swc/core-linux-arm64-gnu": "1.12.14",
"@swc/core-linux-x64-gnu": "1.12.14",
"@swc/html-darwin-arm64": "1.12.14",
"@swc/html-linux-arm64-gnu": "1.12.14",
"@swc/html-linux-x64-gnu": "1.12.14",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"