Compare commits

..

4 Commits

Author SHA1 Message Date
13c8cbf03a fix rac tests 2025-03-28 12:55:14 -03:00
1776981f29 Add schema.yml 2025-03-27 18:18:28 -03:00
5a4df95011 Fix tests, add more tests 2025-03-27 18:17:28 -03:00
f2927e5725 first approach 2025-03-27 18:03:25 -03:00
360 changed files with 2841 additions and 6826 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.2.4
current_version = 2025.2.2
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@ -17,8 +17,6 @@ optional_value = final
[bumpversion:file:pyproject.toml]
[bumpversion:file:uv.lock]
[bumpversion:file:package.json]
[bumpversion:file:docker-compose.yml]

1
.gitignore vendored
View File

@ -33,7 +33,6 @@ eggs/
lib64/
parts/
dist/
out/
sdist/
var/
wheels/

View File

@ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
ARG TARGETOS
ARG TARGETARCH
@ -76,7 +76,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/authentik ./cmd/server
# Stage 4: MaxMind GeoIP
@ -94,9 +94,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.6.14 AS uv
FROM ghcr.io/astral-sh/uv:0.6.10 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2025.2.4"
__version__ = "2025.2.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -46,7 +46,7 @@ LOGGER = get_logger()
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
"""Cache key where application list for user is saved"""
key = f"{CACHE_PREFIX}app_access/{user_pk}"
key = f"{CACHE_PREFIX}/app_access/{user_pk}"
if page_number:
key += f"/{page_number}"
return key

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from rest_framework import mixins
from rest_framework.fields import ReadOnlyField, SerializerMethodField
from rest_framework.fields import SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.object_types import TypesMixin
@ -18,10 +18,10 @@ from authentik.core.models import Provider
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
"""Provider Serializer"""
assigned_application_slug = ReadOnlyField(source="application.slug")
assigned_application_name = ReadOnlyField(source="application.name")
assigned_backchannel_application_slug = ReadOnlyField(source="backchannel_application.slug")
assigned_backchannel_application_name = ReadOnlyField(source="backchannel_application.name")
assigned_application_slug = SerializerMethodField()
assigned_application_name = SerializerMethodField()
assigned_backchannel_application_slug = SerializerMethodField()
assigned_backchannel_application_name = SerializerMethodField()
component = SerializerMethodField()
@ -31,6 +31,38 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
return ""
return obj.component
def get_assigned_application_slug(self, obj: Provider) -> str:
"""Get application slug, return empty string if no application exists"""
try:
return obj.application.slug
except Provider.application.RelatedObjectDoesNotExist:
return ""
def get_assigned_application_name(self, obj: Provider) -> str:
"""Get application name, return empty string if no application exists"""
try:
return obj.application.name
except Provider.application.RelatedObjectDoesNotExist:
return ""
def get_assigned_backchannel_application_slug(self, obj: Provider) -> str:
"""Get backchannel application slug.
Returns an empty string if no backchannel application exists.
"""
if not obj.backchannel_application:
return ""
return obj.backchannel_application.slug or ""
def get_assigned_backchannel_application_name(self, obj: Provider) -> str:
"""Get backchannel application name.
Returns an empty string if no backchannel application exists.
"""
if not obj.backchannel_application:
return ""
return obj.backchannel_application.name or ""
class Meta:
model = Provider
fields = [

View File

@ -179,13 +179,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
"user",
"source",
"source_obj",
"identifier",
"created",
"last_updated",
]
extra_kwargs = {
"created": {"read_only": True},
"last_updated": {"read_only": True},
}
@ -202,7 +199,7 @@ class UserSourceConnectionViewSet(
queryset = UserSourceConnection.objects.all()
serializer_class = UserSourceConnectionSerializer
filterset_fields = ["user", "source__slug"]
search_fields = ["user__username", "source__slug", "identifier"]
search_fields = ["source__slug"]
ordering = ["source__slug", "pk"]
owner_field = "user"
@ -221,11 +218,9 @@ class GroupSourceConnectionSerializer(SourceSerializer):
"source_obj",
"identifier",
"created",
"last_updated",
]
extra_kwargs = {
"created": {"read_only": True},
"last_updated": {"read_only": True},
}
@ -242,5 +237,6 @@ class GroupSourceConnectionViewSet(
queryset = GroupSourceConnection.objects.all()
serializer_class = GroupSourceConnectionSerializer
filterset_fields = ["group", "source__slug"]
search_fields = ["group__name", "source__slug", "identifier"]
search_fields = ["source__slug"]
ordering = ["source__slug", "pk"]
owner_field = "user"

View File

@ -1,14 +1,13 @@
"""User API Views"""
from datetime import timedelta
from importlib import import_module
from json import loads
from typing import Any
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.contrib.sessions.backends.base import SessionBase
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.db.models.functions import ExtractHour
from django.db.transaction import atomic
from django.db.utils import IntegrityError
@ -92,7 +91,6 @@ from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
class UserGroupSerializer(ModelSerializer):
@ -228,7 +226,6 @@ class UserSerializer(ModelSerializer):
"name",
"is_active",
"last_login",
"date_joined",
"is_superuser",
"groups",
"groups_obj",
@ -243,7 +240,6 @@ class UserSerializer(ModelSerializer):
]
extra_kwargs = {
"name": {"allow_blank": True},
"date_joined": {"read_only": True},
"password_change_date": {"read_only": True},
}
@ -377,7 +373,7 @@ class UsersFilter(FilterSet):
method="filter_attributes",
)
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
uuid = UUIDFilter(field_name="uuid")
path = CharFilter(field_name="path")
@ -395,11 +391,6 @@ class UsersFilter(FilterSet):
queryset=Group.objects.all().order_by("name"),
)
def filter_is_superuser(self, queryset, name, value):
if value:
return queryset.filter(ak_groups__is_superuser=True).distinct()
return queryset.exclude(ak_groups__is_superuser=True).distinct()
def filter_attributes(self, queryset, name, value):
"""Filter attributes by query args"""
try:
@ -778,8 +769,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not instance.is_active:
sessions = AuthenticatedSession.objects.filter(user=instance)
session_ids = sessions.values_list("session_key", flat=True)
for session in session_ids:
SessionStore(session).delete()
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
sessions.delete()
LOGGER.debug("Deleted user's sessions", user=instance.username)
return response

View File

@ -1,19 +0,0 @@
# Generated by Django 5.0.13 on 2025-04-07 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0043_alter_group_options"),
]
operations = [
migrations.AddField(
model_name="usersourceconnection",
name="new_identifier",
field=models.TextField(default=""),
preserve_default=False,
),
]

View File

@ -1,30 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0044_usersourceconnection_new_identifier"),
("authentik_sources_kerberos", "0003_migrate_userkerberossourceconnection_identifier"),
("authentik_sources_oauth", "0009_migrate_useroauthsourceconnection_identifier"),
("authentik_sources_plex", "0005_migrate_userplexsourceconnection_identifier"),
("authentik_sources_saml", "0019_migrate_usersamlsourceconnection_identifier"),
]
operations = [
migrations.RenameField(
model_name="usersourceconnection",
old_name="new_identifier",
new_name="identifier",
),
migrations.AddIndex(
model_name="usersourceconnection",
index=models.Index(fields=["identifier"], name="authentik_c_identif_59226f_idx"),
),
migrations.AddIndex(
model_name="usersourceconnection",
index=models.Index(
fields=["source", "identifier"], name="authentik_c_source__649e04_idx"
),
),
]

View File

@ -761,17 +761,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
@property
def component(self) -> str:
"""Return component used to edit this object"""
if self.managed == self.MANAGED_INBUILT:
return ""
raise NotImplementedError
@property
def property_mapping_type(self) -> "type[PropertyMapping]":
"""Return property mapping type used by this object"""
if self.managed == self.MANAGED_INBUILT:
from authentik.core.models import PropertyMapping
return PropertyMapping
raise NotImplementedError
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
@ -786,14 +780,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a user to build final properties upon."""
if self.managed == self.MANAGED_INBUILT:
return {}
raise NotImplementedError
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a group to build final properties upon."""
if self.managed == self.MANAGED_INBUILT:
return {}
raise NotImplementedError
def __str__(self):
@ -824,7 +814,6 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
source = models.ForeignKey(Source, on_delete=models.CASCADE)
identifier = models.TextField()
objects = InheritanceManager()
@ -838,10 +827,6 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
class Meta:
unique_together = (("user", "source"),)
indexes = (
models.Index(fields=("identifier",)),
models.Index(fields=("source", "identifier")),
)
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):

View File

@ -1,10 +1,7 @@
"""authentik core signals"""
from importlib import import_module
from django.conf import settings
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.base import SessionBase
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
@ -28,7 +25,6 @@ password_changed = Signal()
login_failed = Signal()
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
@receiver(post_save, sender=Application)
@ -64,7 +60,8 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
@receiver(pre_delete, sender=AuthenticatedSession)
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted"""
SessionStore(instance.session_key).delete()
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)
@receiver(pre_save)

View File

@ -133,6 +133,8 @@ class TestApplicationsAPI(APITestCase):
"provider_obj": {
"assigned_application_name": "allowed",
"assigned_application_slug": "allowed",
"assigned_backchannel_application_name": "",
"assigned_backchannel_application_slug": "",
"authentication_flow": None,
"invalidation_flow": None,
"authorization_flow": str(self.provider.authorization_flow.pk),
@ -186,6 +188,8 @@ class TestApplicationsAPI(APITestCase):
"provider_obj": {
"assigned_application_name": "allowed",
"assigned_application_slug": "allowed",
"assigned_backchannel_application_name": "",
"assigned_backchannel_application_slug": "",
"authentication_flow": None,
"invalidation_flow": None,
"authorization_flow": str(self.provider.authorization_flow.pk),

View File

@ -3,7 +3,8 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import PropertyMapping
from authentik.core.api.providers import ProviderSerializer
from authentik.core.models import Application, PropertyMapping, Provider
from authentik.core.tests.utils import create_test_admin_user
@ -24,3 +25,51 @@ class TestProvidersAPI(APITestCase):
reverse("authentik_api:provider-types"),
)
self.assertEqual(response.status_code, 200)
def test_provider_serializer_without_application(self):
"""Test that Provider serializer handles missing application gracefully"""
# Create a provider without an application
provider = Provider.objects.create(name="test-provider")
serializer = ProviderSerializer(instance=provider)
serialized_data = serializer.data
# Check that fields return empty strings when no application exists
self.assertEqual(serialized_data["assigned_application_slug"], "")
self.assertEqual(serialized_data["assigned_application_name"], "")
self.assertEqual(serialized_data["assigned_backchannel_application_slug"], "")
self.assertEqual(serialized_data["assigned_backchannel_application_name"], "")
def test_provider_serializer_with_application(self):
"""Test that Provider serializer correctly includes application data"""
# Create an application
app = Application.objects.create(name="Test App", slug="test-app")
# Create a provider with an application
provider = Provider.objects.create(name="test-provider-with-app")
app.provider = provider
app.save()
serializer = ProviderSerializer(instance=provider)
serialized_data = serializer.data
# Check that fields return correct values when application exists
self.assertEqual(serialized_data["assigned_application_slug"], "test-app")
self.assertEqual(serialized_data["assigned_application_name"], "Test App")
self.assertEqual(serialized_data["assigned_backchannel_application_slug"], "")
self.assertEqual(serialized_data["assigned_backchannel_application_name"], "")
def test_provider_api_response(self):
"""Test that the API response includes empty strings for missing applications"""
# Create a provider without an application
provider = Provider.objects.create(name="test-provider-api")
response = self.client.get(
reverse("authentik_api:provider-detail", kwargs={"pk": provider.pk}),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["assigned_application_slug"], "")
self.assertEqual(response.data["assigned_application_name"], "")
self.assertEqual(response.data["assigned_backchannel_application_slug"], "")
self.assertEqual(response.data["assigned_backchannel_application_name"], "")

View File

@ -1,19 +0,0 @@
from django.apps import apps
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
class TestSourceAPI(APITestCase):
def setUp(self) -> None:
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_builtin_source_used_by(self):
"""Test Providers's types endpoint"""
apps.get_app_config("authentik_core").source_inbuilt()
response = self.client.get(
reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}),
)
self.assertEqual(response.status_code, 200)

View File

@ -1,7 +1,6 @@
"""Test Users API"""
from datetime import datetime
from json import loads
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
@ -16,12 +15,7 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.core.tests.utils import (
create_test_admin_user,
create_test_brand,
create_test_flow,
create_test_user,
)
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.email.models import EmailStage
@ -32,7 +26,7 @@ class TestUsersAPI(APITestCase):
def setUp(self) -> None:
self.admin = create_test_admin_user()
self.user = create_test_user()
self.user = User.objects.create(username="test-user")
def test_filter_type(self):
"""Test API filtering by type"""
@ -47,35 +41,6 @@ class TestUsersAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
def test_filter_is_superuser(self):
"""Test API filtering by superuser status"""
User.objects.all().delete()
admin = create_test_admin_user()
self.client.force_login(admin)
# Test superuser
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": True,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1)
self.assertEqual(body["results"][0]["username"], admin.username)
# Test non-superuser
user = create_test_user()
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": False,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1, body)
self.assertEqual(body["results"][0]["username"], user.username)
def test_list_with_groups(self):
"""Test listing with groups"""
self.client.force_login(self.admin)
@ -134,8 +99,6 @@ class TestUsersAPI(APITestCase):
def test_recovery_email_no_flow(self):
"""Test user recovery link (no recovery flow set)"""
self.client.force_login(self.admin)
self.user.email = ""
self.user.save()
response = self.client.post(
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
)

View File

@ -13,11 +13,7 @@ from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.property_mappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet
from authentik.core.api.sources import (
GroupSourceConnectionViewSet,
SourceViewSet,
UserSourceConnectionViewSet,
)
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.transactional_applications import TransactionalApplicationView
from authentik.core.api.users import UserViewSet
@ -85,7 +81,6 @@ api_urlpatterns = [
("core/tokens", TokenViewSet),
("sources/all", SourceViewSet),
("sources/user_connections/all", UserSourceConnectionViewSet),
("sources/group_connections/all", GroupSourceConnectionViewSet),
("providers/all", ProviderViewSet),
("propertymappings/all", PropertyMappingViewSet),
("authenticators/all", DeviceViewSet, "device"),

View File

@ -69,7 +69,6 @@ SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
SESSION_KEY_GET = "authentik/flows/get"
SESSION_KEY_POST = "authentik/flows/post"
SESSION_KEY_HISTORY = "authentik/flows/history"
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
QS_KEY_TOKEN = "flow_token" # nosec
QS_QUERY = "query"
@ -454,7 +453,6 @@ class FlowExecutorView(APIView):
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,
SESSION_KEY_GET,
SESSION_KEY_AUTH_STARTED,
# We might need the initial POST payloads for later requests
# SESSION_KEY_POST,
# We don't delete the history on purpose, as a user might

View File

@ -6,22 +6,14 @@ from django.shortcuts import get_object_or_404
from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
from authentik.flows.models import Flow
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["flow"] = flow
if (
not self.request.user.is_authenticated
and flow.designation == FlowDesignation.AUTHENTICATION
):
self.request.session[SESSION_KEY_AUTH_STARTED] = True
self.request.session.save()
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)

View File

@ -18,15 +18,6 @@ class SerializerModel(models.Model):
@property
def serializer(self) -> type[BaseSerializer]:
"""Get serializer for this model"""
# Special handling for built-in source
if (
hasattr(self, "managed")
and hasattr(self, "MANAGED_INBUILT")
and self.managed == self.MANAGED_INBUILT
):
from authentik.core.api.sources import SourceSerializer
return SourceSerializer
raise NotImplementedError

View File

@ -35,4 +35,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
label = "authentik_policies"
verbose_name = "authentik Policies"
default = True
mountpoint = "policy/"

View File

@ -1,89 +0,0 @@
{% extends 'login/base_full.html' %}
{% load static %}
{% load i18n %}
{% block head %}
{{ block.super }}
<script>
let redirecting = false;
const checkAuth = async () => {
if (redirecting) return true;
const url = "{{ check_auth_url }}";
console.debug("authentik/policies/buffer: Checking authentication...");
try {
const result = await fetch(url, {
method: "HEAD",
});
if (result.status >= 400) {
return false
}
console.debug("authentik/policies/buffer: Continuing");
redirecting = true;
if ("{{ auth_req_method }}" === "post") {
document.querySelector("form").submit();
} else {
window.location.assign("{{ continue_url|escapejs }}");
}
} catch {
return false;
}
};
let timeout = 100;
let offset = 20;
let attempt = 0;
const main = async () => {
attempt += 1;
await checkAuth();
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
setTimeout(main, timeout);
timeout += (offset * attempt);
if (timeout >= 2000) {
timeout = 2000;
}
}
document.addEventListener("visibilitychange", async () => {
if (document.hidden) return;
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
await checkAuth();
});
main();
</script>
{% endblock %}
{% block title %}
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
{% endblock %}
{% block card_title %}
{% trans 'Waiting for authentication...' %}
{% endblock %}
{% block card %}
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
{% if auth_req_method == "post" %}
{% for key, value in auth_req_body.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
{% endif %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<div class="pf-c-empty-state__icon">
<span class="pf-c-spinner pf-m-xl" role="progressbar">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
<h1 class="pf-c-title pf-m-lg">
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
</h1>
</div>
</div>
<div class="pf-c-form__group pf-m-action">
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
{% trans "Authenticate in this tab" %}
</a>
</div>
</form>
{% endblock %}

View File

@ -1,121 +0,0 @@
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpResponse
from django.test import RequestFactory, TestCase
from django.urls import reverse
from authentik.core.models import Application, Provider
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.flows.models import FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response
from authentik.policies.views import (
QS_BUFFER_ID,
SESSION_KEY_BUFFER,
BufferedPolicyAccessView,
BufferView,
PolicyAccessView,
)
class TestPolicyViews(TestCase):
"""Test PolicyAccessView"""
def setUp(self):
super().setUp()
self.factory = RequestFactory()
self.user = create_test_user()
def test_pav(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
class TestView(PolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = self.user
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.content, b"foo")
def test_pav_buffer(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
def test_pav_buffer_skip(self):
"""Test simple policy access view (skip buffer)"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/?skip_buffer=true")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
def test_buffer(self):
"""Test buffer view"""
uid = generate_id()
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
ts = generate_id()
req.session[SESSION_KEY_BUFFER % uid] = {
"method": "get",
"body": {},
"url": f"/{ts}",
}
req.session.save()
res = BufferView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertIn(ts, res.render().content.decode())

View File

@ -1,14 +1,7 @@
"""API URLs"""
from django.urls import path
from authentik.policies.api.bindings import PolicyBindingViewSet
from authentik.policies.api.policies import PolicyViewSet
from authentik.policies.views import BufferView
urlpatterns = [
path("buffer", BufferView.as_view(), name="buffer"),
]
api_urlpatterns = [
("policies/all", PolicyViewSet),

View File

@ -1,37 +1,23 @@
"""authentik access helper classes"""
from typing import Any
from uuid import uuid4
from django.contrib import messages
from django.contrib.auth.mixins import AccessMixin
from django.contrib.auth.views import redirect_to_login
from django.http import HttpRequest, HttpResponse, QueryDict
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlencode
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views.generic.base import TemplateView, View
from django.views.generic.base import View
from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider, User
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import (
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_AUTH_STARTED,
SESSION_KEY_PLAN,
SESSION_KEY_POST,
)
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
QS_BUFFER_ID = "af_bf_id"
QS_SKIP_BUFFER = "skip_buffer"
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
class RequestValidationError(SentryIgnoredException):
@ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View):
for message in result.messages:
messages.error(self.request, _(message))
return result
def url_with_qs(url: str, **kwargs):
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
parameters are retained"""
if "?" not in url:
return url + f"?{urlencode(kwargs)}"
url, _, qs = url.partition("?")
qs = QueryDict(qs, mutable=True)
qs.update(kwargs)
return url + f"?{urlencode(qs.items())}"
class BufferView(TemplateView):
"""Buffer view"""
template_name = "policies/buffer.html"
def get_context_data(self, **kwargs):
buf_id = self.request.GET.get(QS_BUFFER_ID)
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
kwargs["auth_req_method"] = buffer["method"]
kwargs["auth_req_body"] = buffer["body"]
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
return super().get_context_data(**kwargs)
class BufferedPolicyAccessView(PolicyAccessView):
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
def handle_no_permission(self):
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED)
if plan:
flow = Flow.objects.filter(pk=plan.flow_pk).first()
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
return super().handle_no_permission()
if not plan and authenticating is None:
LOGGER.debug("Not buffering request, no flow plan active")
return super().handle_no_permission()
if self.request.GET.get(QS_SKIP_BUFFER):
LOGGER.debug("Not buffering request, explicit skip")
return super().handle_no_permission()
buffer_id = str(uuid4())
LOGGER.debug("Buffering access request", bf_id=buffer_id)
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
"body": self.request.POST,
"url": self.request.build_absolute_uri(self.request.get_full_path()),
"method": self.request.method.lower(),
}
return redirect(
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
)
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if QS_BUFFER_ID in self.request.GET:
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
return response

View File

@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError
from authentik.policies.views import PolicyAccessView, RequestValidationError
from authentik.providers.oauth2.constants import (
PKCE_METHOD_PLAIN,
PKCE_METHOD_S256,
@ -328,7 +328,7 @@ class OAuthAuthorizationParams:
return code
class AuthorizationFlowInitView(BufferedPolicyAccessView):
class AuthorizationFlowInitView(PolicyAccessView):
"""OAuth2 Flow initializer, checks access to application and starts flow"""
params: OAuthAuthorizationParams

View File

@ -74,6 +74,8 @@ class TestEndpointsAPI(APITestCase):
"component": "ak-provider-rac-form",
"assigned_application_slug": self.app.slug,
"assigned_application_name": self.app.name,
"assigned_backchannel_application_slug": "",
"assigned_backchannel_application_name": "",
"verbose_name": "RAC Provider",
"verbose_name_plural": "RAC Providers",
"meta_model_name": "authentik_providers_rac.racprovider",
@ -124,6 +126,8 @@ class TestEndpointsAPI(APITestCase):
"component": "ak-provider-rac-form",
"assigned_application_slug": self.app.slug,
"assigned_application_name": self.app.name,
"assigned_backchannel_application_slug": "",
"assigned_backchannel_application_name": "",
"connection_expiry": "hours=8",
"delete_token_on_disconnect": False,
"verbose_name": "RAC Provider",
@ -153,6 +157,8 @@ class TestEndpointsAPI(APITestCase):
"component": "ak-provider-rac-form",
"assigned_application_slug": self.app.slug,
"assigned_application_name": self.app.name,
"assigned_backchannel_application_slug": "",
"assigned_backchannel_application_name": "",
"connection_expiry": "hours=8",
"delete_token_on_disconnect": False,
"verbose_name": "RAC Provider",

View File

@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
from authentik.policies.views import BufferedPolicyAccessView
from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
class RACStartView(BufferedPolicyAccessView):
class RACStartView(PolicyAccessView):
"""Start a RAC connection by checking access and creating a connection token"""
endpoint: Endpoint

View File

@ -1,22 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-31 13:50
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0017_samlprovider_authn_context_class_ref_mapping"),
]
operations = [
migrations.AlterField(
model_name="samlprovider",
name="acs_url",
field=models.TextField(
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="ACS URL",
),
),
]

View File

@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
from authentik.core.api.object_types import CreatableType
from authentik.core.models import PropertyMapping, Provider
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import (
DSA_SHA1,
@ -41,9 +40,7 @@ class SAMLBindings(models.TextChoices):
class SAMLProvider(Provider):
"""SAML 2.0 Endpoint for applications which support SAML."""
acs_url = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL")
)
acs_url = models.URLField(verbose_name=_("ACS URL"))
audience = models.TextField(
default="",
blank=True,

View File

@ -15,7 +15,7 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_POST
from authentik.lib.views import bad_request_message
from authentik.policies.views import BufferedPolicyAccessView
from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
@ -35,7 +35,7 @@ from authentik.stages.consent.stage import (
LOGGER = get_logger()
class SAMLSSOView(BufferedPolicyAccessView):
class SAMLSSOView(PolicyAccessView):
"""SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler."""
@ -83,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""GET and POST use the same handler, but we can't
override .dispatch easily because BufferedPolicyAccessView's dispatch"""
override .dispatch easily because PolicyAccessView's dispatch"""
return self.get(request, application_slug)

View File

@ -28,8 +28,8 @@ def pytest_report_header(*_, **__):
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
current_id = int(environ.get("CI_RUN_ID", "0")) - 1
total_ids = int(environ.get("CI_TOTAL_RUNS", "0"))
current_id = int(environ.get("CI_RUN_ID", 0)) - 1
total_ids = int(environ.get("CI_TOTAL_RUNS", 0))
if total_ids:
num_tests = len(items)

View File

@ -1,11 +1,13 @@
"""Kerberos Source Serializer"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import (
GroupSourceConnectionSerializer,
GroupSourceConnectionViewSet,
UserSourceConnectionSerializer,
UserSourceConnectionViewSet,
)
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.kerberos.models import (
GroupKerberosSourceConnection,
UserKerberosSourceConnection,
@ -13,20 +15,33 @@ from authentik.sources.kerberos.models import (
class UserKerberosSourceConnectionSerializer(UserSourceConnectionSerializer):
class Meta(UserSourceConnectionSerializer.Meta):
"""Kerberos Source Serializer"""
class Meta:
model = UserKerberosSourceConnection
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
class UserKerberosSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset"""
queryset = UserKerberosSourceConnection.objects.all()
serializer_class = UserKerberosSourceConnectionSerializer
filterset_fields = ["source__slug"]
search_fields = ["source__slug"]
ordering = ["source__slug"]
owner_field = "user"
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
"""OAuth Group-Source connection Serializer"""
class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupKerberosSourceConnection
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet):
"""Group-source connection Viewset"""
queryset = GroupKerberosSourceConnection.objects.all()
serializer_class = GroupKerberosSourceConnectionSerializer

View File

@ -1,28 +0,0 @@
from django.db import migrations
def migrate_identifier(apps, schema_editor):
db_alias = schema_editor.connection.alias
UserKerberosSourceConnection = apps.get_model(
"authentik_sources_kerberos", "UserKerberosSourceConnection"
)
for connection in UserKerberosSourceConnection.objects.using(db_alias).all():
connection.new_identifier = connection.identifier
connection.save(using=db_alias)
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_kerberos", "0002_kerberossource_kadmin_type"),
("authentik_core", "0044_usersourceconnection_new_identifier"),
]
operations = [
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
migrations.RemoveField(
model_name="userkerberossourceconnection",
name="identifier",
),
]

View File

@ -372,6 +372,8 @@ class KerberosSourcePropertyMapping(PropertyMapping):
class UserKerberosSourceConnection(UserSourceConnection):
"""Connection to configured Kerberos Sources."""
identifier = models.TextField()
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.kerberos.api.source_connection import (

View File

@ -99,7 +99,6 @@ class LDAPSourceSerializer(SourceSerializer):
"sync_groups",
"sync_parent_group",
"connectivity",
"lookup_groups_from_user",
]
extra_kwargs = {"bind_password": {"write_only": True}}
@ -135,7 +134,6 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"sync_parent_group",
"user_property_mappings",
"group_property_mappings",
"lookup_groups_from_user",
]
search_fields = ["name", "slug"]
ordering = ["name"]

View File

@ -1,24 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-26 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_sources_ldap",
"0006_rename_ldappropertymapping_ldapsourcepropertymapping_and_more",
),
]
operations = [
migrations.AddField(
model_name="ldapsource",
name="lookup_groups_from_user",
field=models.BooleanField(
default=False,
help_text="Lookup group membership based on a user attribute instead of a group attribute. This allows nested group resolution on systems like FreeIPA and Active Directory",
),
),
]

View File

@ -123,14 +123,6 @@ class LDAPSource(Source):
Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT
)
lookup_groups_from_user = models.BooleanField(
default=False,
help_text=_(
"Lookup group membership based on a user attribute instead of a group attribute. "
"This allows nested group resolution on systems like FreeIPA and Active Directory"
),
)
@property
def component(self) -> str:
return "ak-source-ldap-form"

View File

@ -28,17 +28,15 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
if not self._source.sync_groups:
self.message("Group syncing is disabled for this Source")
return iter(())
# If we are looking up groups from users, we don't need to fetch the group membership field
attributes = [self._source.object_uniqueness_field, LDAP_DISTINGUISHED_NAME]
if not self._source.lookup_groups_from_user:
attributes.append(self._source.group_membership_field)
return self.search_paginator(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=SUBTREE,
attributes=attributes,
attributes=[
self._source.group_membership_field,
self._source.object_uniqueness_field,
LDAP_DISTINGUISHED_NAME,
],
**kwargs,
)
@ -49,24 +47,9 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
return -1
membership_count = 0
for group in page_data:
if self._source.lookup_groups_from_user:
group_dn = group.get("dn", {})
group_filter = f"({self._source.group_membership_field}={group_dn})"
group_members = self._source.connection().extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=group_filter,
search_scope=SUBTREE,
attributes=[self._source.object_uniqueness_field],
)
members = []
for group_member in group_members:
group_member_dn = group_member.get("dn", {})
members.append(group_member_dn)
else:
if "attributes" not in group:
continue
members = group.get("attributes", {}).get(self._source.group_membership_field, [])
if "attributes" not in group:
continue
members = group.get("attributes", {}).get(self._source.group_membership_field, [])
ak_group = self.get_group(group)
if not ak_group:
continue
@ -85,7 +68,7 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
"ak_groups__in": [ak_group],
}
)
).distinct()
)
membership_count += 1
membership_count += users.count()
ak_group.users.set(users)

View File

@ -96,26 +96,6 @@ def mock_freeipa_connection(password: str) -> Connection:
"objectClass": "posixAccount",
},
)
# User with groups in memberOf attribute
connection.strategy.add_entry(
"cn=user4,ou=users,dc=goauthentik,dc=io",
{
"name": "user4_sn",
"uid": "user4_sn",
"objectClass": "person",
"memberOf": [
"cn=reverse-lookup-group,ou=groups,dc=goauthentik,dc=io",
],
},
)
connection.strategy.add_entry(
"cn=reverse-lookup-group,ou=groups,dc=goauthentik,dc=io",
{
"cn": "reverse-lookup-group",
"uid": "reverse-lookup-group",
"objectClass": "groupOfNames",
},
)
# Locked out user
connection.strategy.add_entry(
"cn=user-nsaccountlock,ou=users,dc=goauthentik,dc=io",

View File

@ -162,43 +162,6 @@ class LDAPSyncTests(TestCase):
self.assertFalse(User.objects.filter(username="user1_sn").exists())
self.assertFalse(User.objects.get(username="user-nsaccountlock").is_active)
def test_sync_groups_freeipa_memberOf(self):
"""Test group sync when membership is derived from memberOf user attribute"""
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.lookup_groups_from_user = True
self.source.group_membership_field = "memberOf"
self.source.user_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
)
)
self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
managed="goauthentik.io/sources/ldap/openldap-cn"
)
)
connection = MagicMock(return_value=mock_freeipa_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
user_sync = UserLDAPSynchronizer(self.source)
user_sync.sync_full()
group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync_full()
membership_sync = MembershipLDAPSynchronizer(self.source)
membership_sync.sync_full()
self.assertTrue(
User.objects.filter(username="user4_sn").exists(), "User does not exist"
)
# Test if membership mapping based on memberOf works.
memberof_group = Group.objects.filter(name="reverse-lookup-group")
self.assertTrue(memberof_group.exists(), "Group does not exist")
self.assertTrue(
memberof_group.first().users.filter(username="user4_sn").exists(),
"User not a member of the group",
)
def test_sync_groups_ad(self):
"""Test group sync"""
self.source.user_property_mappings.set(

View File

@ -1,3 +1,5 @@
"""OAuth Source Serializer"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import (
@ -10,9 +12,11 @@ from authentik.sources.oauth.models import GroupOAuthSourceConnection, UserOAuth
class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
"""OAuth Source Serializer"""
class Meta(UserSourceConnectionSerializer.Meta):
model = UserOAuthSourceConnection
fields = UserSourceConnectionSerializer.Meta.fields + ["access_token"]
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier", "access_token"]
extra_kwargs = {
**UserSourceConnectionSerializer.Meta.extra_kwargs,
"access_token": {"write_only": True},
@ -20,15 +24,21 @@ class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
class UserOAuthSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
"""Source Viewset"""
queryset = UserOAuthSourceConnection.objects.all()
serializer_class = UserOAuthSourceConnectionSerializer
class GroupOAuthSourceConnectionSerializer(GroupSourceConnectionSerializer):
"""OAuth Group-Source connection Serializer"""
class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupOAuthSourceConnection
class GroupOAuthSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
"""Group-source connection Viewset"""
queryset = GroupOAuthSourceConnection.objects.all()
serializer_class = GroupOAuthSourceConnectionSerializer

View File

@ -1,28 +0,0 @@
from django.db import migrations
def migrate_identifier(apps, schema_editor):
db_alias = schema_editor.connection.alias
UserOAuthSourceConnection = apps.get_model(
"authentik_sources_oauth", "UserOAuthSourceConnection"
)
for connection in UserOAuthSourceConnection.objects.using(db_alias).all():
connection.new_identifier = connection.identifier
connection.save(using=db_alias)
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_oauth", "0008_groupoauthsourceconnection_and_more"),
("authentik_core", "0044_usersourceconnection_new_identifier"),
]
operations = [
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
migrations.RemoveField(
model_name="useroauthsourceconnection",
name="identifier",
),
]

View File

@ -286,6 +286,7 @@ class OAuthSourcePropertyMapping(PropertyMapping):
class UserOAuthSourceConnection(UserSourceConnection):
"""Authorized remote OAuth provider."""
identifier = models.CharField(max_length=255)
access_token = models.TextField(blank=True, null=True, default=None)
@property

View File

@ -1,3 +1,5 @@
"""Plex Source connection Serializer"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import (
@ -10,9 +12,14 @@ from authentik.sources.plex.models import GroupPlexSourceConnection, UserPlexSou
class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
"""Plex Source connection Serializer"""
class Meta(UserSourceConnectionSerializer.Meta):
model = UserPlexSourceConnection
fields = UserSourceConnectionSerializer.Meta.fields + ["plex_token"]
fields = UserSourceConnectionSerializer.Meta.fields + [
"identifier",
"plex_token",
]
extra_kwargs = {
**UserSourceConnectionSerializer.Meta.extra_kwargs,
"plex_token": {"write_only": True},
@ -20,15 +27,21 @@ class UserPlexSourceConnectionSerializer(UserSourceConnectionSerializer):
class UserPlexSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
"""Plex Source connection Serializer"""
queryset = UserPlexSourceConnection.objects.all()
serializer_class = UserPlexSourceConnectionSerializer
class GroupPlexSourceConnectionSerializer(GroupSourceConnectionSerializer):
"""Plex Group-Source connection Serializer"""
class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupPlexSourceConnection
class GroupPlexSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
"""Group-source connection Viewset"""
queryset = GroupPlexSourceConnection.objects.all()
serializer_class = GroupPlexSourceConnectionSerializer

View File

@ -1,29 +0,0 @@
from django.db import migrations
def migrate_identifier(apps, schema_editor):
db_alias = schema_editor.connection.alias
UserPlexSourceConnection = apps.get_model("authentik_sources_plex", "UserPlexSourceConnection")
for connection in UserPlexSourceConnection.objects.using(db_alias).all():
connection.new_identifier = connection.identifier
connection.save(using=db_alias)
class Migration(migrations.Migration):
dependencies = [
(
"authentik_sources_plex",
"0004_groupplexsourceconnection_plexsourcepropertymapping_and_more",
),
("authentik_core", "0044_usersourceconnection_new_identifier"),
]
operations = [
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
migrations.RemoveField(
model_name="userplexsourceconnection",
name="identifier",
),
]

View File

@ -141,6 +141,7 @@ class UserPlexSourceConnection(UserSourceConnection):
"""Connect user and plex source"""
plex_token = models.TextField()
identifier = models.TextField()
@property
def serializer(self) -> type[Serializer]:

View File

@ -1,3 +1,5 @@
"""SAML Source Serializer"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import (
@ -10,20 +12,29 @@ from authentik.sources.saml.models import GroupSAMLSourceConnection, UserSAMLSou
class UserSAMLSourceConnectionSerializer(UserSourceConnectionSerializer):
"""SAML Source Serializer"""
class Meta(UserSourceConnectionSerializer.Meta):
model = UserSAMLSourceConnection
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
class UserSAMLSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
"""Source Viewset"""
queryset = UserSAMLSourceConnection.objects.all()
serializer_class = UserSAMLSourceConnectionSerializer
class GroupSAMLSourceConnectionSerializer(GroupSourceConnectionSerializer):
"""OAuth Group-Source connection Serializer"""
class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupSAMLSourceConnection
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
class GroupSAMLSourceConnectionViewSet(GroupSourceConnectionViewSet):
"""Group-source connection Viewset"""
queryset = GroupSAMLSourceConnection.objects.all()
serializer_class = GroupSAMLSourceConnectionSerializer

View File

@ -1,35 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-31 13:53
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_saml", "0017_fix_x509subjectname"),
]
operations = [
migrations.AlterField(
model_name="samlsource",
name="slo_url",
field=models.TextField(
blank=True,
default=None,
help_text="Optional URL if your IDP supports Single-Logout.",
null=True,
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="SLO URL",
),
),
migrations.AlterField(
model_name="samlsource",
name="sso_url",
field=models.TextField(
help_text="URL that the initial Login request is sent to.",
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="SSO URL",
),
),
]

View File

@ -1,26 +0,0 @@
from django.db import migrations
def migrate_identifier(apps, schema_editor):
db_alias = schema_editor.connection.alias
UserSAMLSourceConnection = apps.get_model("authentik_sources_saml", "UserSAMLSourceConnection")
for connection in UserSAMLSourceConnection.objects.using(db_alias).all():
connection.new_identifier = connection.identifier
connection.save(using=db_alias)
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_saml", "0018_alter_samlsource_slo_url_alter_samlsource_sso_url"),
("authentik_core", "0044_usersourceconnection_new_identifier"),
]
operations = [
migrations.RunPython(code=migrate_identifier, reverse_code=migrations.RunPython.noop),
migrations.RemoveField(
model_name="usersamlsourceconnection",
name="identifier",
),
]

View File

@ -20,7 +20,6 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.models import Flow
from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.models import DomainlessURLValidator
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import (
DSA_SHA1,
@ -92,13 +91,11 @@ class SAMLSource(Source):
help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
)
sso_url = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
sso_url = models.URLField(
verbose_name=_("SSO URL"),
help_text=_("URL that the initial Login request is sent to."),
)
slo_url = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
slo_url = models.URLField(
default=None,
blank=True,
null=True,
@ -318,6 +315,8 @@ class SAMLSourcePropertyMapping(PropertyMapping):
class UserSAMLSourceConnection(UserSourceConnection):
"""Connection to configured SAML Sources."""
identifier = models.TextField()
@property
def serializer(self) -> Serializer:
from authentik.sources.saml.api.source_connection import UserSAMLSourceConnectionSerializer

File diff suppressed because one or more lines are too long

View File

@ -104,13 +104,6 @@ def send_mail(
# can't be converted to json)
message_object.attach(logo_data())
if (
message_object.to
and isinstance(message_object.to[0], str)
and "=?utf-8?" in message_object.to[0]
):
message_object.to = [message_object.to[0].split("<")[-1].replace(">", "")]
LOGGER.debug("Sending mail", to=message_object.to)
backend.send_messages([message_object])
Event.new(

View File

@ -97,37 +97,6 @@ class TestEmailStageSending(FlowTestCase):
self.assertEqual(mail.outbox[0].subject, "authentik")
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
def test_utf8_name(self):
"""Test with pending user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
utf8_user = create_test_user()
utf8_user.name = "Cirilo ЉМНЊ el cirilico И̂ӢЙӤ "
utf8_user.email = "cyrillic@authentik.local"
utf8_user.save()
plan.context[PLAN_CONTEXT_PENDING_USER] = utf8_user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
):
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")
self.assertEqual(mail.outbox[0].to, [f"{utf8_user.email}"])
def test_pending_fake_user(self):
"""Test with pending (fake) user"""
self.flow.designation = FlowDesignation.RECOVERY

View File

@ -142,38 +142,35 @@ class IdentificationChallengeResponse(ChallengeResponse):
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
# Password check
if current_stage.password_stage:
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
# Captcha check
if captcha_stage := current_stage.captcha_stage:
captcha_token = attrs.get("captcha_token", None)
if not captcha_token:
self.stage.logger.warning("Token not set for captcha attempt")
verify_captcha_token(captcha_stage, captcha_token, client_ip)
# Password check
if not current_stage.password_stage:
# No password stage select, don't validate the password
return attrs
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
return attrs

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2025.2.4 Blueprint schema",
"title": "authentik 2025.2.2 Blueprint schema",
"required": [
"version",
"entries"
@ -6423,6 +6423,8 @@
},
"acs_url": {
"type": "string",
"format": "uri",
"maxLength": 200,
"minLength": 1,
"title": "ACS URL"
},
@ -7885,11 +7887,6 @@
"type": "string",
"format": "uuid",
"title": "Sync parent group"
},
"lookup_groups_from_user": {
"type": "boolean",
"title": "Lookup groups from user",
"description": "Lookup group membership based on a user attribute instead of a group attribute. This allows nested group resolution on systems like FreeIPA and Active Directory"
}
},
"required": []
@ -8236,6 +8233,7 @@
},
"identifier": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"title": "Identifier"
},
@ -8735,6 +8733,8 @@
},
"sso_url": {
"type": "string",
"format": "uri",
"maxLength": 200,
"minLength": 1,
"title": "SSO URL",
"description": "URL that the initial Login request is sent to."
@ -8744,6 +8744,8 @@
"string",
"null"
],
"format": "uri",
"maxLength": 200,
"title": "SLO URL",
"description": "Optional URL if your IDP supports Single-Logout."
},

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
restart: unless-stopped
command: server
environment:
@ -54,7 +54,7 @@ services:
redis:
condition: service_healthy
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.4}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
restart: unless-stopped
command: worker
environment:

17
go.mod
View File

@ -1,10 +1,12 @@
module goauthentik.io
go 1.24.0
go 1.23.0
toolchain go1.24.0
require (
beryju.io/ldap v0.1.0
github.com/coreos/go-oidc/v3 v3.14.1
github.com/coreos/go-oidc/v3 v3.13.0
github.com/getsentry/sentry-go v0.31.1
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.10
@ -20,17 +22,17 @@ require (
github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.8.0
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.7.3
github.com/sethvargo/go-envconfig v1.1.1
github.com/sirupsen/logrus v1.9.3
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.2025024.1
goauthentik.io/api/v3 v3.2025022.6
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.13.0
golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0
gopkg.in/yaml.v2 v2.4.0
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
)
@ -60,6 +62,7 @@ require (
github.com/go-openapi/validate v0.24.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect
@ -76,6 +79,6 @@ require (
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

31
go.sum
View File

@ -55,8 +55,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -148,9 +148,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@ -208,8 +207,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -240,8 +239,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
@ -300,8 +299,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.2025024.1 h1:wYmpbNW1XptrjS5dlnZj8CrCs+JUGEVJYStrFdWL9aA=
goauthentik.io/api/v3 v3.2025024.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025022.6 h1:M5M8Cd/1N7E8KLkvYYh7VdcdKz5nfzjKPFLK+YOtOVg=
goauthentik.io/api/v3 v3.2025022.6/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=
@ -396,8 +395,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -412,8 +411,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -600,8 +599,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2025.2.4"
const VERSION = "2025.2.2"

View File

@ -0,0 +1,5 @@
//go:build requirefips
package backend
var FipsEnabled = true

View File

@ -0,0 +1,5 @@
//go:build !requirefips
package backend
var FipsEnabled = false

View File

@ -2,7 +2,6 @@ package ak
import (
"context"
"crypto/fips140"
"fmt"
"math/rand"
"net/http"
@ -204,7 +203,7 @@ func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
"golangVersion": runtime.Version(),
"opensslEnabled": cryptobackend.OpensslEnabled,
"opensslVersion": cryptobackend.OpensslVersion(),
"fipsEnabled": fips140.Enabled(),
"fipsEnabled": cryptobackend.FipsEnabled,
}
hostname, err := os.Hostname()
if err == nil {

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
@ -27,7 +27,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/ldap ./cmd/ldap
# Stage 2: Run

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "MIT",
"devDependencies": {
"aws-cdk": "^2.1007.0",
"aws-cdk": "^2.1006.0",
"cross-env": "^7.0.3"
},
"engines": {
@ -17,9 +17,9 @@
}
},
"node_modules/aws-cdk": {
"version": "2.1007.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1007.0.tgz",
"integrity": "sha512-/UOYOTGWUm+pP9qxg03tID5tL6euC+pb+xo0RBue+xhnUWwj/Bbsw6DbqbpOPMrNzTUxmM723/uMEQmM6S26dw==",
"version": "2.1006.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz",
"integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@ -10,7 +10,7 @@
"node": ">=20"
},
"devDependencies": {
"aws-cdk": "^2.1007.0",
"aws-cdk": "^2.1006.0",
"cross-env": "^7.0.3"
}
}

View File

@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2025.2.4
Default: 2025.2.2
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -1220,20 +1220,6 @@ msgstr ""
msgid "Reputation Scores"
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr ""

View File

@ -10,8 +10,8 @@
# Manuel Viens, 2023
# Mordecai, 2023
# nerdinator <florian.dupret@gmail.com>, 2024
# Tina, 2024
# Charles Leclerc, 2025
# Tina, 2025
# Marc Schmitt, 2025
#
#, fuzzy
@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -1347,22 +1347,6 @@ msgstr "Score de Réputation"
msgid "Reputation Scores"
msgstr "Scores de Réputation"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "En attente de l'authentification..."
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
"Vous êtes déjà en cours d'authentification dans un autre onglet. Cette page "
"se rafraîchira lorsque l'authentification sera terminée."
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr "S'authentifier dans cet onglet"
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr "Permission refusée"

View File

@ -6,23 +6,23 @@
# Translators:
# Dario Rigolin, 2022
# aoor9, 2023
# Matteo Piccina <altermatte@gmail.com>, 2024
# Enrico Campani, 2024
# Marco Vitale, 2024
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2024
# Nicola Mersi, 2024
# tmassimi, 2024
# Marc Schmitt, 2024
# albanobattistella <albanobattistella@gmail.com>, 2024
# Matteo Piccina <altermatte@gmail.com>, 2025
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
"POT-Creation-Date: 2025-02-14 14:49+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
"Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2024\n"
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -130,10 +130,6 @@ msgstr "L'utente non ha accesso all'applicazione."
msgid "Extra description not available"
msgstr "Descrizione extra non disponibile"
#: authentik/core/api/groups.py
msgid "Cannot set group as parent of itself."
msgstr "Impossibile impostare il gruppo come padre di se stesso."
#: authentik/core/api/providers.py
msgid ""
"When not set all providers are returned. When set to true, only backchannel "
@ -181,14 +177,6 @@ msgstr "Aggiungi utente al gruppo"
msgid "Remove user from group"
msgstr "Rimuovi l'utente dal gruppo"
#: authentik/core/models.py
msgid "Enable superuser status"
msgstr "Abilita stato di superutente"
#: authentik/core/models.py
msgid "Disable superuser status"
msgstr "Disabilita stato di superutente"
#: authentik/core/models.py
msgid "User's display name."
msgstr "Nome visualizzato dell'utente."
@ -272,11 +260,11 @@ msgstr "Applicazioni"
#: authentik/core/models.py
msgid "Application Entitlement"
msgstr "Entitlement Applicazione"
msgstr ""
#: authentik/core/models.py
msgid "Application Entitlements"
msgstr "Entitlements Applicazione"
msgstr ""
#: authentik/core/models.py
msgid "Use the source-specific identifier"
@ -563,6 +551,62 @@ msgstr "Mappatura Microsoft Entra Provider"
msgid "Microsoft Entra Provider Mappings"
msgstr "Mappature Microsoft Entra Provider"
#: authentik/enterprise/providers/rac/models.py
#: authentik/stages/user_login/models.py
msgid ""
"Determines how long a session lasts. Default of 0 means that the sessions "
"lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
msgstr ""
"Determina quanto può durare una sessione. Se impostato a 0, la sessione "
"durerà fino alla chiusura del browser. (Formato: "
"hours=-1;minutes=-2;seconds=-3)"
#: authentik/enterprise/providers/rac/models.py
msgid "When set to true, connection tokens will be deleted upon disconnect."
msgstr ""
"Se impostato su vero, i token di connessione verranno eliminati alla "
"disconnessione."
#: authentik/enterprise/providers/rac/models.py
msgid "RAC Provider"
msgstr "Fornitore di controllo dell'accesso remoto"
#: authentik/enterprise/providers/rac/models.py
msgid "RAC Providers"
msgstr "Fornitori di controllo dell'accesso remoto"
#: authentik/enterprise/providers/rac/models.py
msgid "RAC Endpoint"
msgstr "Endpoint RAC"
#: authentik/enterprise/providers/rac/models.py
msgid "RAC Endpoints"
msgstr "Endpoints RAC"
#: authentik/enterprise/providers/rac/models.py
msgid "RAC Provider Property Mapping"
msgstr "Mappatura delle proprietà del provider RAC"
#: authentik/enterprise/providers/rac/models.py
msgid "RAC Provider Property Mappings"
msgstr "Mappature proprietà del provider RAC"
#: authentik/enterprise/providers/rac/models.py
msgid "RAC Connection token"
msgstr "RAC Connection token"
#: authentik/enterprise/providers/rac/models.py
msgid "RAC Connection tokens"
msgstr "RAC Connection tokens"
#: authentik/enterprise/providers/rac/views.py
msgid "Maximum connection limit reached."
msgstr "Limite massimo di connessioni raggiunto."
#: authentik/enterprise/providers/rac/views.py
msgid "(You are already connected in another tab/window)"
msgstr "(Sei già connesso in un'altra scheda/finestra)"
#: authentik/enterprise/providers/ssf/models.py
#: authentik/providers/oauth2/models.py
msgid "Signing Key"
@ -570,39 +614,39 @@ msgstr "Chiave di firma"
#: authentik/enterprise/providers/ssf/models.py
msgid "Key used to sign the SSF Events."
msgstr "Chiave utilizzata per firmare gli eventi SSF."
msgstr ""
#: authentik/enterprise/providers/ssf/models.py
msgid "Shared Signals Framework Provider"
msgstr "Fornitore Shared Signals Framework"
msgstr ""
#: authentik/enterprise/providers/ssf/models.py
msgid "Shared Signals Framework Providers"
msgstr "Fornitori Shared Signals Framework"
msgstr ""
#: authentik/enterprise/providers/ssf/models.py
msgid "Add stream to SSF provider"
msgstr "Aggiungi Stream al provider SSF"
msgstr ""
#: authentik/enterprise/providers/ssf/models.py
msgid "SSF Stream"
msgstr "SSF Stream"
msgstr ""
#: authentik/enterprise/providers/ssf/models.py
msgid "SSF Streams"
msgstr "SSF Streams"
msgstr ""
#: authentik/enterprise/providers/ssf/models.py
msgid "SSF Stream Event"
msgstr "Evento di Stream SSF"
msgstr ""
#: authentik/enterprise/providers/ssf/models.py
msgid "SSF Stream Events"
msgstr "Eventi di Stream SSF"
msgstr ""
#: authentik/enterprise/providers/ssf/tasks.py
msgid "Failed to send request"
msgstr "Impossibile inviare la richiesta"
msgstr ""
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
@ -668,26 +712,9 @@ msgid "Slack Webhook (Slack/Discord)"
msgstr "Slack Webhook (Slack/Discord)"
#: authentik/events/models.py
#: authentik/stages/authenticator_validate/models.py
msgid "Email"
msgstr "Email"
#: authentik/events/models.py
msgid ""
"Customize the body of the request. Mapping should return data that is JSON-"
"serializable."
msgstr ""
"Personalizza il corpo della richiesta. Il mapping dovrebbe restituire dati "
"serializzabili in JSON."
#: authentik/events/models.py
msgid ""
"Configure additional headers to be sent. Mapping should return a dictionary "
"of key-value pairs"
msgstr ""
"Configurare le intestazioni aggiuntive da inviare. Il mapping dovrebbe "
"restituire un dizionario di coppie chiave-valore."
#: authentik/events/models.py
msgid ""
"Only send notification once, for example when sending a webhook into a chat "
@ -917,7 +944,7 @@ msgstr ""
#: authentik/flows/models.py
msgid "Evaluate policies when the Stage is presented to the user."
msgstr "Valutare i criteri quando la fase viene presentata all'utente."
msgstr ""
#: authentik/flows/models.py
msgid ""
@ -959,14 +986,6 @@ msgstr "Tokens del flusso"
msgid "Invalid next URL"
msgstr "URL successivo non valido"
#: authentik/lib/sync/outgoing/models.py
msgid ""
"When enabled, provider will not modify or create objects in the remote "
"system."
msgstr ""
"Quando abilitato, il provider non modificherà o creerà oggetti nel sistema "
"remoto."
#: authentik/lib/sync/outgoing/tasks.py
msgid "Starting full provider sync"
msgstr "Avvio della sincronizzazione completa del provider"
@ -981,10 +1000,6 @@ msgstr "Sincronizzando pagina {page} degli utenti"
msgid "Syncing page {page} of groups"
msgstr "Sincronizzando pagina {page} dei gruppi"
#: authentik/lib/sync/outgoing/tasks.py
msgid "Dropping mutating request due to dry run"
msgstr "Richiesta di mutazione ignorata a causa della prova di funzionamento"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
msgid "Stopping sync due to error: {error}"
@ -1197,14 +1212,6 @@ msgstr "GeoIP: indirizzo IP del client non trovato nel database della città."
msgid "Client IP is not in an allowed country."
msgstr "L'IP del client non si trova in un paese consentito."
#: authentik/policies/geoip/models.py
msgid "Distance from previous authentication is larger than threshold."
msgstr "La distanza dall'autenticazione precedente è maggiore della soglia."
#: authentik/policies/geoip/models.py
msgid "Distance is further than possible."
msgstr "La distanza è maggiore del possibile."
#: authentik/policies/geoip/models.py
msgid "GeoIP Policy"
msgstr "Criterio GeoIP"
@ -1337,22 +1344,6 @@ msgstr "Punteggio di reputazione"
msgid "Reputation Scores"
msgstr "Punteggi di reputazione"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "In attesa di autenticazione..."
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
"Ti stai già autenticando in un'altra scheda. Questa pagina si aggiornerà una"
" volta completata l'autenticazione."
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr "Autenticati in questa scheda"
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr "Permesso negato"
@ -1540,14 +1531,6 @@ msgstr "RS256 (Crittografia Asimmetrica)"
msgid "ES256 (Asymmetric Encryption)"
msgstr "ES256 (Crittografia Asimmetrica)"
#: authentik/providers/oauth2/models.py
msgid "ES384 (Asymmetric Encryption)"
msgstr "ES384 (Crittografia Asimmetrica)"
#: authentik/providers/oauth2/models.py
msgid "ES512 (Asymmetric Encryption)"
msgstr "ES512 (Crittografia Asimmetrica)"
#: authentik/providers/oauth2/models.py
msgid "Scope used by the client"
msgstr "Scope usato dall'utente"
@ -1831,61 +1814,6 @@ msgstr "Provider Proxy"
msgid "Proxy Providers"
msgstr "Providers Proxy"
#: authentik/providers/rac/models.py authentik/stages/user_login/models.py
msgid ""
"Determines how long a session lasts. Default of 0 means that the sessions "
"lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
msgstr ""
"Determina quanto può durare una sessione. Se impostato a 0, la sessione "
"durerà fino alla chiusura del browser. (Formato: "
"hours=-1;minutes=-2;seconds=-3)"
#: authentik/providers/rac/models.py
msgid "When set to true, connection tokens will be deleted upon disconnect."
msgstr ""
"Se impostato su vero, i token di connessione verranno eliminati alla "
"disconnessione."
#: authentik/providers/rac/models.py
msgid "RAC Provider"
msgstr "Fornitore di controllo dell'accesso remoto"
#: authentik/providers/rac/models.py
msgid "RAC Providers"
msgstr "Fornitori di controllo dell'accesso remoto"
#: authentik/providers/rac/models.py
msgid "RAC Endpoint"
msgstr "Endpoint RAC"
#: authentik/providers/rac/models.py
msgid "RAC Endpoints"
msgstr "Endpoints RAC"
#: authentik/providers/rac/models.py
msgid "RAC Provider Property Mapping"
msgstr "Mappatura delle proprietà del provider RAC"
#: authentik/providers/rac/models.py
msgid "RAC Provider Property Mappings"
msgstr "Mappature proprietà del provider RAC"
#: authentik/providers/rac/models.py
msgid "RAC Connection token"
msgstr "RAC Connection token"
#: authentik/providers/rac/models.py
msgid "RAC Connection tokens"
msgstr "RAC Connection tokens"
#: authentik/providers/rac/views.py
msgid "Maximum connection limit reached."
msgstr "Limite massimo di connessioni raggiunto."
#: authentik/providers/rac/views.py
msgid "(You are already connected in another tab/window)"
msgstr "(Sei già connesso in un'altra scheda/finestra)"
#: authentik/providers/radius/models.py
msgid "Shared secret between clients and server to hash packets."
msgstr "Segreto condiviso tra client e server per hashare i pacchetti."
@ -1973,20 +1901,6 @@ msgstr ""
"Configura il modo in cui verrà creato il valore NameID. Se lasciato vuoto, "
"verrà considerato il NameIDPolicy della richiesta in arrivo"
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr "Mapping delle proprietà AuthnContextClassRef"
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
"Configura come verrà creato il valore AuthnContextClassRef. Se lasciato "
"vuoto, AuthnContextClassRef verrà impostato in base ai metodi di "
"autenticazione utilizzati dall'utente."
#: authentik/providers/saml/models.py
msgid ""
"Assertion valid not before current time + this value (Format: "
@ -2128,18 +2042,6 @@ msgstr "Provider SAML dai Metadati"
msgid "SAML Providers from Metadata"
msgstr "Providers SAML dai Metadati"
#: authentik/providers/scim/models.py
msgid "Default"
msgstr "Predefinito"
#: authentik/providers/scim/models.py
msgid "AWS"
msgstr "AWS"
#: authentik/providers/scim/models.py
msgid "Slack"
msgstr "Slack"
#: authentik/providers/scim/models.py
msgid "Base URL to SCIM requests, usually ends in /v2"
msgstr "URL di base per le richieste SCIM, di solito termina con /v2"
@ -2148,16 +2050,6 @@ msgstr "URL di base per le richieste SCIM, di solito termina con /v2"
msgid "Authentication token"
msgstr "Token di autenticazione"
#: authentik/providers/scim/models.py
msgid "SCIM Compatibility Mode"
msgstr "SCIM Modalità di Compatibilità"
#: authentik/providers/scim/models.py
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
msgstr ""
"Modifica il comportamento di autenticazione per le implementazioni SCIM "
"specifiche del fornitore."
#: authentik/providers/scim/models.py
msgid "SCIM Provider"
msgstr "Privider SCIM"
@ -2233,7 +2125,7 @@ msgstr ""
#: authentik/sources/kerberos/models.py
msgid "KAdmin server type"
msgstr "Tipo server KAdmin"
msgstr ""
#: authentik/sources/kerberos/models.py
msgid "Sync users from Kerberos into authentik"
@ -2838,117 +2730,6 @@ msgstr "Dispositivo Duo"
msgid "Duo Devices"
msgstr "Dispositivi Duo"
#: authentik/stages/authenticator_email/models.py
msgid "Email OTP"
msgstr "Email OTP"
#: authentik/stages/authenticator_email/models.py
#: authentik/stages/email/models.py
msgid ""
"When enabled, global Email connection settings will be used and connection "
"settings below will be ignored."
msgstr ""
"Se abilitato, verranno utilizzate le impostazioni di connessione e-mail "
"globali e le impostazioni di connessione riportate di seguito verranno "
"ignorate."
#: authentik/stages/authenticator_email/models.py
#: authentik/stages/email/models.py
msgid "Time the token sent is valid (Format: hours=3,minutes=17,seconds=300)."
msgstr ""
"Tempo di validità del token inviato (formato: "
"hours=3,minutes=17,seconds=300)."
#: authentik/stages/authenticator_email/models.py
msgid "Email Authenticator Setup Stage"
msgstr "Fase di configurazione dell'autenticatore email"
#: authentik/stages/authenticator_email/models.py
msgid "Email Authenticator Setup Stages"
msgstr "Fasi di configurazione dell'autenticatore email"
#: authentik/stages/authenticator_email/models.py
#: authentik/stages/authenticator_email/stage.py
#: authentik/stages/email/stage.py
msgid "Exception occurred while rendering E-mail template"
msgstr ""
"Eccezione verificatasi durante la visualizzazione del modello di posta "
"elettronica"
#: authentik/stages/authenticator_email/models.py
msgid "Email Device"
msgstr "Dispositivo email"
#: authentik/stages/authenticator_email/models.py
msgid "Email Devices"
msgstr "Dispositivi email"
#: authentik/stages/authenticator_email/stage.py
#: authentik/stages/authenticator_sms/stage.py
#: authentik/stages/authenticator_totp/stage.py
msgid "Code does not match"
msgstr "Il codice non corrisponde"
#: authentik/stages/authenticator_email/stage.py
msgid "Invalid email"
msgstr "Email non valida"
#: authentik/stages/authenticator_email/templates/email/email_otp.html
#: authentik/stages/email/templates/email/password_reset.html
#, python-format
msgid ""
"\n"
" Hi %(username)s,\n"
" "
msgstr ""
"\n"
" Ciao %(username)s,\n"
" "
#: authentik/stages/authenticator_email/templates/email/email_otp.html
msgid ""
"\n"
" Email MFA code.\n"
" "
msgstr ""
"\n"
" Codice MFA via e-mail.\n"
" "
#: authentik/stages/authenticator_email/templates/email/email_otp.html
#, python-format
msgid ""
"\n"
" If you did not request this code, please ignore this email. The code above is valid for %(expires)s.\n"
" "
msgstr ""
"\n"
" Se non hai richiesto questo codice, ignora questa email. Il codice sopra riportato è valido per %(expires)s.\n"
" "
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
#: authentik/stages/email/templates/email/password_reset.txt
#, python-format
msgid "Hi %(username)s,"
msgstr "Ciao %(username)s,"
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
msgid ""
"\n"
"Email MFA code\n"
msgstr ""
"\n"
"Codice e-mail MFA\n"
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
#, python-format
msgid ""
"\n"
"If you did not request this code, please ignore this email. The code above is valid for %(expires)s.\n"
msgstr ""
"\n"
"Se non hai richiesto questo codice, ignora questa email. Il codice sopra riportato è valido per %(expires)s.\n"
#: authentik/stages/authenticator_sms/models.py
msgid ""
"When enabled, the Phone number is only used during enrollment to verify the "
@ -2987,6 +2768,11 @@ msgstr "Dispositivo SMS"
msgid "SMS Devices"
msgstr "Dispositivi SMS"
#: authentik/stages/authenticator_sms/stage.py
#: authentik/stages/authenticator_totp/stage.py
msgid "Code does not match"
msgstr "Il codice non corrisponde"
#: authentik/stages/authenticator_sms/stage.py
msgid "Invalid phone number"
msgstr "Numero di telefono non valido"
@ -3227,10 +3013,23 @@ msgstr "Ripristino password"
msgid "Account Confirmation"
msgstr "Conferma dell'account"
#: authentik/stages/email/models.py
msgid ""
"When enabled, global Email connection settings will be used and connection "
"settings below will be ignored."
msgstr ""
"Se abilitato, verranno utilizzate le impostazioni di connessione e-mail "
"globali e le impostazioni di connessione riportate di seguito verranno "
"ignorate."
#: authentik/stages/email/models.py
msgid "Activate users upon completion of stage."
msgstr "Attiva gli utenti al completamento della fase."
#: authentik/stages/email/models.py
msgid "Time in minutes the token sent is valid."
msgstr "Tempo in minuti in cui il token inviato è valido."
#: authentik/stages/email/models.py
msgid "Email Stage"
msgstr "Fase email"
@ -3239,6 +3038,12 @@ msgstr "Fase email"
msgid "Email Stages"
msgstr "Fasi Email"
#: authentik/stages/email/stage.py
msgid "Exception occurred while rendering E-mail template"
msgstr ""
"Eccezione verificatasi durante la visualizzazione del modello di posta "
"elettronica"
#: authentik/stages/email/stage.py
msgid "Successfully verified Email."
msgstr "Email verificato con successo."
@ -3322,6 +3127,17 @@ msgstr ""
"\n"
"Questa email è stata inviata dal trasporto delle notifiche %(name)s.\n"
#: authentik/stages/email/templates/email/password_reset.html
#, python-format
msgid ""
"\n"
" Hi %(username)s,\n"
" "
msgstr ""
"\n"
" Ciao %(username)s,\n"
" "
#: authentik/stages/email/templates/email/password_reset.html
msgid ""
"\n"
@ -3342,6 +3158,11 @@ msgstr ""
" Se non hai richiesto una modifica della password, ignora questa e-mail. Il link sopra è valido per %(expires)s.\n"
" "
#: authentik/stages/email/templates/email/password_reset.txt
#, python-format
msgid "Hi %(username)s,"
msgstr "Ciao %(username)s,"
#: authentik/stages/email/templates/email/password_reset.txt
msgid ""
"\n"
@ -3671,7 +3492,6 @@ msgstr ""
#: authentik/stages/redirect/api.py
msgid "Target Flow should be present when mode is Flow."
msgstr ""
"Il flusso target dovrebbe essere presente quando la modalità è Flusso."
#: authentik/stages/redirect/models.py
msgid "Redirect Stage"

Binary file not shown.

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
"POT-Creation-Date: 2025-03-22 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -1234,20 +1234,6 @@ msgstr "信誉分数"
msgid "Reputation Scores"
msgstr "信誉分数"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "正在等待身份验证…"
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr "您正在另一个标签页中验证身份。身份验证完成后,此页面会刷新。"
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr "在此标签页中验证身份"
#: authentik/policies/templates/policies/denied.html
msgid "Permission denied"
msgstr "权限被拒绝"

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2025.2.4",
"version": "2025.2.2",
"private": true
}

View File

@ -1,11 +0,0 @@
/**
* @file TypeScript type definitions for eslint-plugin-react-hooks
*/
declare module "eslint-plugin-react-hooks" {
import { ESLint } from "eslint";
// We have to do this because ESLint aliases the namespace and class simultaneously.
type PluginInstance = ESLint.Plugin;
const Plugin: PluginInstance;
export default Plugin;
}

View File

@ -1,11 +0,0 @@
/**
* @file TypeScript type definitions for eslint-plugin-react
*/
declare module "eslint-plugin-react" {
import { ESLint } from "eslint";
// We have to do this because ESLint aliases the namespace and class simultaneously.
type PluginInstance = ESLint.Plugin;
const Plugin: PluginInstance;
export default Plugin;
}

View File

@ -1,18 +0,0 @@
The MIT License (MIT)
Copyright (c) 2025 Authentik Security, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,5 +0,0 @@
# `@goauthentik/eslint-config`
This package contains the ESLint configuration used by authentik.
While it is possible to use this configuration outside of our projects,
you may find that it is not as useful as other popular configurations.

View File

@ -1,72 +0,0 @@
import eslint from "@eslint/js";
import { javaScriptConfig } from "@goauthentik/eslint-config/javascript-config";
import { reactConfig } from "@goauthentik/eslint-config/react-config";
import { typescriptConfig } from "@goauthentik/eslint-config/typescript-config";
import * as litconf from "eslint-plugin-lit";
import * as wcconf from "eslint-plugin-wc";
import tseslint from "typescript-eslint";
// @ts-check
/**
* @typedef ESLintPackageConfigOptions Options for creating package ESLint configuration.
* @property {string[]} [ignorePatterns] Override ignore patterns for ESLint.
*/
/**
* @type {string[]} Default Ignore patterns for ESLint.
*/
export const DefaultIgnorePatterns = [
// ---
"**/*.md",
"**/out",
"**/dist",
"**/.wireit",
"website/build/**",
"website/.docusaurus/**",
"**/node_modules",
"**/coverage",
"**/storybook-static",
"**/locale-codes.ts",
"**/src/locales",
"**/gen-ts-api",
];
/**
* Given a preferred package name, creates a ESLint configuration object.
*
* @param {ESLintPackageConfigOptions} options The preferred package configuration options.
*
* @returns The ESLint configuration object.
*/
export function createESLintPackageConfig({ ignorePatterns = DefaultIgnorePatterns } = {}) {
return tseslint.config(
{
ignores: ignorePatterns,
},
eslint.configs.recommended,
javaScriptConfig,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
...typescriptConfig,
...reactConfig,
{
rules: {
"no-console": "off",
},
files: [
// ---
"**/scripts/**/*",
"**/test/**/*",
"**/tests/**/*",
],
},
);
}

View File

@ -1,143 +0,0 @@
// @ts-check
import tseslint from "typescript-eslint";
const MAX_DEPTH = 4;
const MAX_NESTED_CALLBACKS = 4;
const MAX_PARAMS = 5;
/**
* ESLint configuration for JavaScript authentik projects.
*/
export const javaScriptConfig = tseslint.config({
rules: {
// TODO: Clean up before enabling.
"accessor-pairs": "off",
"array-callback-return": "error",
"block-scoped-var": "error",
"consistent-return": ["error", { treatUndefinedAsUnspecified: false }],
"consistent-this": ["error", "that"],
"curly": "off",
"dot-notation": [
"error",
{
allowKeywords: true,
},
],
"eqeqeq": "error",
"func-names": ["error", "as-needed"],
"guard-for-in": "error",
"max-depth": ["error", MAX_DEPTH],
"max-nested-callbacks": ["error", MAX_NESTED_CALLBACKS],
"max-params": ["error", MAX_PARAMS],
// TODO: Clean up before enabling.
// "new-cap": "error",
"no-alert": "error",
"no-array-constructor": "error",
"no-bitwise": [
"error",
{
allow: ["~"],
int32Hint: true,
},
],
"no-caller": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-div-regex": "error",
"no-dupe-args": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-else-return": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-function": ["error", { allow: ["constructors"] }],
"no-labels": "error",
"no-eq-null": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-boolean-cast": "error",
"no-extra-label": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-implied-eval": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-inner-declarations": ["error", "functions"],
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-multi-str": "error",
// TODO: Clean up before enabling.
"no-negated-condition": "off",
"no-new": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-param-reassign": ["error", { props: false }],
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-restricted-syntax": ["error", "WithStatement"],
"no-script-url": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
// TODO: Clean up before enabling.
// "no-shadow": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "off", // Handled by Prettier.
"no-undef": "off",
"no-undef-init": "off",
"no-unexpected-multiline": "error",
"no-useless-constructor": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-use-before-define": "error",
"no-useless-call": "error",
"no-dupe-class-members": "error",
"no-var": "error",
"no-void": "error",
"no-with": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"radix": "error",
"require-yield": "error",
"strict": ["error", "global"],
"use-isnan": "error",
"valid-typeof": "error",
"vars-on-top": "error",
"yoda": ["error", "never"],
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
// SonarJS is not yet compatible with ESLint 9. Commenting these out
// until it is.
// "sonarjs/cognitive-complexity": ["off", MAX_COGNITIVE_COMPLEXITY],
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
},
});
export default javaScriptConfig;

View File

@ -1,53 +0,0 @@
{
"name": "@goauthentik/eslint-config",
"version": "1.0.0",
"description": "authentik's ESLint config",
"license": "MIT",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./index.js",
"types": "./out/index.d.ts"
},
"./react-config": {
"import": "./react-config.js",
"types": "./out/react-config.d.ts"
},
"./javascript-config": {
"import": "./javascript-config.js",
"types": "./out/javascript-config.d.ts"
},
"./typescript-config": {
"import": "./typescript-config.js",
"types": "./out/typescript-config.d.ts"
}
},
"dependencies": {
"eslint": "^9.23.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0"
},
"devDependencies": {
"@goauthentik/tsconfig": "1.0.0",
"@types/eslint": "^9.6.1",
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.0"
},
"peerDependencies": {
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.0"
},
"optionalDependencies": {
"react": "^18.3.1"
},
"engines": {
"node": ">=20.11"
},
"types": "./out/index.d.ts",
"prettier": "@goauthentik/prettier-config",
"publishConfig": {
"access": "public"
}
}

View File

@ -1,34 +0,0 @@
import reactPlugin from "eslint-plugin-react";
import hooksPlugin from "eslint-plugin-react-hooks";
import tseslint from "typescript-eslint";
/**
* ESLint configuration for React authentik projects.
*/
export const reactConfig = tseslint.config({
settings: {
react: {
version: "detect",
},
},
plugins: {
"react": reactPlugin,
"react-hooks": hooksPlugin,
},
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/jsx-uses-react": 0,
"react/display-name": "off",
"react/jsx-curly-brace-presence": "error",
"react/jsx-no-leaked-render": "error",
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
},
});
export default reactConfig;

View File

@ -1,8 +0,0 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true
}
}

View File

@ -1,35 +0,0 @@
// @ts-check
import tseslint from "typescript-eslint";
/**
* ESLint configuration for TypeScript authentik projects.
*/
export const typescriptConfig = tseslint.config({
rules: {
"@typescript-eslint/ban-ts-comment": [
"error",
{
"ts-expect-error": "allow-with-description",
"ts-ignore": true,
"ts-nocheck": "allow-with-description",
"ts-check": false,
"minimumDescriptionLength": 5,
},
],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "error",
"no-invalid-this": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
});
export default typescriptConfig;

View File

@ -1,18 +0,0 @@
The MIT License (MIT)
Copyright (c) 2025 Authentik Security, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,5 +0,0 @@
# `@goauthentik/monorepo`
This package contains utility scripts common to all TypeScript and JavaScript packages in the
`@goauthentik` monorepo.

View File

@ -1,17 +0,0 @@
/**
* @file Constants for JavaScript and TypeScript files.
*
*/
/**
* The current Node.js environment, defaulting to "development" when not set.
*
* Note, this should only be used during the build process.
*
* If you need to check the environment at runtime, use `process.env.NODE_ENV` to
* ensure that module tree-shaking works correctly.
*
*/
export const NodeEnvironment = /** @type {'development' | 'production'} */ (
process.env.NODE_ENV || "development"
);

View File

@ -1,4 +0,0 @@
export * from "./paths.js";
export * from "./constants.js";
export * from "./version.js";
export * from "./scripting.js";

View File

@ -1,19 +0,0 @@
{
"name": "@goauthentik/monorepo",
"version": "1.0.0",
"description": "Utilities for the authentik monorepo.",
"private": true,
"license": "MIT",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./index.js",
"types": "./out/index.d.ts"
}
},
"types": "./out/index.d.ts",
"engines": {
"node": ">=20.11"
}
}

View File

@ -1,30 +0,0 @@
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @typedef {'~authentik'} MonoRepoRoot
*/
/**
* The root of the authentik monorepo.
*/
export const MonoRepoRoot = /** @type {MonoRepoRoot} */ (resolve(__dirname, "..", ".."));
const require = createRequire(import.meta.url);
/**
* Resolve a package name to its location in the monorepo to the single node_modules directory.
* @param {string} packageName
* @returns {string} The resolved path to the package.
* @throws {Error} If the package cannot be resolved.
*/
export function resolvePackage(packageName) {
const packageJSONPath = require.resolve(join(packageName, "package.json"), {
paths: [MonoRepoRoot],
});
return dirname(packageJSONPath);
}

View File

@ -1,40 +0,0 @@
import { createRequire } from "node:module";
import * as path from "node:path";
import * as process from "node:process";
import { fileURLToPath } from "node:url";
/**
* Predicate to determine if a module was run directly, i.e. not imported.
*
* @param {ImportMeta} meta The `import.meta` object of the module.
*
* @return {boolean} Whether the module was run directly.
*/
export function isMain(meta) {
// Are we not in a module context?
if (!meta) return false;
const relativeScriptPath = process.argv[1];
if (!relativeScriptPath) return false;
const require = createRequire(meta.url);
const absoluteScriptPath = require.resolve(relativeScriptPath);
const modulePath = fileURLToPath(meta.url);
const scriptExtension = path.extname(absoluteScriptPath);
if (scriptExtension) {
return modulePath === absoluteScriptPath;
}
const moduleExtension = path.extname(modulePath);
if (moduleExtension) {
return absoluteScriptPath === modulePath.slice(0, -moduleExtension.length);
}
// If both are without extension, compare them directly.
return modulePath === absoluteScriptPath;
}

View File

@ -1,9 +0,0 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"resolveJsonModule": true,
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true
}
}

View File

@ -1,45 +0,0 @@
import { execSync } from "node:child_process";
import PackageJSON from "../../package.json" with { type: "json" };
import { MonoRepoRoot } from "./paths.js";
/**
* The current version of authentik in SemVer format.
*
*/
export const AuthentikVersion = /**@type {`${number}.${number}.${number}`} */ (PackageJSON.version);
/**
* Reads the last commit hash from the current git repository.
*/
export function readGitBuildHash() {
try {
const commit = execSync("git rev-parse HEAD", {
encoding: "utf8",
cwd: MonoRepoRoot,
})
.toString()
.trim();
return commit;
} catch (_error) {
console.debug("Git commit could not be read.");
}
return process.env.GIT_BUILD_HASH || "";
}
/**
* Reads the build identifier for the current environment.
*
* This must match the behavior defined in authentik's server-side `get_full_version` function.
*
* @see {@link "authentik\_\_init\_\_.py"}
*/
export function readBuildIdentifier() {
const { GIT_BUILD_HASH } = process.env;
if (!GIT_BUILD_HASH) return AuthentikVersion;
return [AuthentikVersion, GIT_BUILD_HASH].join("+");
}

View File

@ -1,18 +0,0 @@
The MIT License (MIT)
Copyright (c) 2025 Authentik Security, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,5 +0,0 @@
# `@goauthentik/prettier-config`
This package contains the Prettier configuration used by authentik.
While it is possible to use this configuration outside of our projects,
you may find that it is not as useful as other popular configurations.

View File

@ -1,80 +0,0 @@
/**
* @file Prettier configuration for authentik.
*
* @import { Config as PrettierConfig } from "prettier";
* @import { PluginConfig as SortPluginConfig } from "@trivago/prettier-plugin-sort-imports";
*
* @typedef {object} PackageJSONPluginConfig
* @property {string[]} [packageSortOrder] Custom ordering array.
*/
/**
* authentik Prettier configuration.
*
* @type {PrettierConfig & SortPluginConfig & PackageJSONPluginConfig}
* @internal
*/
export const AuthentikPrettierConfig = {
arrowParens: "always",
bracketSpacing: true,
embeddedLanguageFormatting: "auto",
htmlWhitespaceSensitivity: "css",
insertPragma: false,
jsxSingleQuote: false,
printWidth: 100,
proseWrap: "preserve",
quoteProps: "consistent",
requirePragma: false,
semi: true,
singleQuote: false,
tabWidth: 4,
trailingComma: "all",
useTabs: false,
vueIndentScriptAndStyle: false,
plugins: ["prettier-plugin-packagejson", "@trivago/prettier-plugin-sort-imports"],
importOrder: ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
importOrderParserPlugins: ["typescript", "jsx", "classProperties", "decorators-legacy"],
overrides: [
{
files: "schemas/**/*.json",
options: {
tabWidth: 2,
},
},
{
files: "tsconfig.json",
options: {
trailingComma: "none",
},
},
{
files: "package.json",
options: {
packageSortOrder: [
// ---
"name",
"version",
"description",
"license",
"private",
"author",
"authors",
"scripts",
"main",
"type",
"exports",
"imports",
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
"wireit",
"resolutions",
"engines",
],
},
},
],
};

View File

@ -1,20 +0,0 @@
import { format } from "prettier";
import { AuthentikPrettierConfig } from "./config.js";
/**
* Format using Prettier.
*
* Defaults to using the TypeScript parser.
*
* @category Formatting
* @param {string} fileContents The contents of the file to format.
*
* @returns {Promise<string>} The formatted file contents.
*/
export function formatWithPrettier(fileContents) {
return format(fileContents, {
...AuthentikPrettierConfig,
parser: "typescript",
});
}

View File

@ -1,6 +0,0 @@
import { AuthentikPrettierConfig } from "./config.js";
export * from "./config.js";
export * from "./format.js";
export default AuthentikPrettierConfig;

View File

@ -1,27 +0,0 @@
{
"name": "@goauthentik/prettier-config",
"version": "1.0.0",
"description": "authentik's Prettier config",
"license": "MIT",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./index.js",
"types": "./out/index.d.ts"
}
},
"types": "./out/index.d.ts",
"peerDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-packagejson": "^2.5.10"
},
"engines": {
"node": ">=20.11"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -1,8 +0,0 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true
}
}

Some files were not shown because too many files have changed in this diff Show More