Compare commits

..

1 Commits

Author SHA1 Message Date
f9e129bed2 web: Apply linter fixes. Update tests. 2025-04-02 19:03:29 +02:00
390 changed files with 3137 additions and 5975 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.2.4
current_version = 2025.2.3
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*))?

1
.gitignore vendored
View File

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

View File

@ -94,7 +94,7 @@ 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.11 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base

View File

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

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

@ -228,7 +228,6 @@ class UserSerializer(ModelSerializer):
"name",
"is_active",
"last_login",
"date_joined",
"is_superuser",
"groups",
"groups_obj",
@ -243,7 +242,6 @@ class UserSerializer(ModelSerializer):
]
extra_kwargs = {
"name": {"allow_blank": True},
"date_joined": {"read_only": True},
"password_change_date": {"read_only": True},
}

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

@ -824,7 +824,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 +837,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

@ -36,6 +36,7 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_values
@ -209,6 +210,8 @@ class SourceFlowManager:
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
flow_context.update(
{
# Since we authenticate the user by their token, they have no backend set

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

@ -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,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

@ -318,6 +318,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

View File

@ -33,6 +33,7 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
@ -73,6 +74,8 @@ class InitiateView(View):
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
kwargs.update(
{
PLAN_CONTEXT_SSO: True,

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

@ -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.3 Blueprint schema",
"required": [
"version",
"entries"
@ -7885,11 +7885,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 +8231,7 @@
},
"identifier": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"title": "Identifier"
},

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.3}
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.3}
restart: unless-stopped
command: worker
environment:

14
go.mod
View File

@ -1,10 +1,9 @@
module goauthentik.io
go 1.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 +19,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.2025023.2
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 +59,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 +76,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.2025023.2 h1:4XHlnykN5jQH78liQ4cp2Jf8eigvQImIJp+A+bsq1nA=
goauthentik.io/api/v3 v3.2025023.2/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.3"

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.3
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

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

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2025.2.4",
"version": "2025.2.3",
"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
}
}

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,6 +0,0 @@
# `@goauthentik/tsconfig`
This package contains the TypeScript configuration used by authentik TypeScript projects.
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,18 +0,0 @@
{
"name": "@goauthentik/tsconfig",
"version": "1.0.0",
"description": "authentik's s base TypeScript configuration.",
"keywords": [
"tsconfig",
"typescript"
],
"license": "MIT",
"type": "module",
"main": "tsconfig.json",
"engines": {
"node": ">=20.11"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -1,29 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"alwaysStrict": true,
"baseUrl": ".",
"composite": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": false,
"isolatedModules": true,
"incremental": true,
"jsx": "react-jsx",
"lib": ["ESNext"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"newLine": "lf",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": false,
"outDir": "${configDir}/out",
"pretty": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"target": "ESNext",
"useUnknownInCatchVariables": true
}
}

View File

@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2025.2.4"
version = "2025.2.3"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.12.*"
@ -52,7 +52,7 @@ dependencies = [
"pydantic-scim",
"pyjwt",
"pyrad",
"python-kadmin-rs ==0.6.0",
"python-kadmin-rs ==0.5.3",
"pyyaml",
"requests-oauthlib",
"scim2-filter-parser",

File diff suppressed because it is too large Load Diff

1316
uv.lock generated

File diff suppressed because it is too large Load Diff

8
web/package-lock.json generated
View File

@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.4-1744139776",
"@goauthentik/api": "^2025.2.3-1743464496",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@ -1835,9 +1835,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2025.2.4-1744139776",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.4-1744139776.tgz",
"integrity": "sha512-4CLjeW19Zc5/WhxOX/welwAI+YvyKp8cR4uBqLCScvyy/fTwB8dH2NnRj4eDv5U+OQpAwdKXZCBY16fWGg0/kg=="
"version": "2025.2.3-1743464496",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.2.3-1743464496.tgz",
"integrity": "sha512-35+SqFNoBZ+WNpyG2Xv/VKYKIIxjwRmIbgX5WZSpc9IlJVv7yyckUYvLpU2F0hZVUMDnxAUE5bsiNn7K4EQslw=="
},
"node_modules/@goauthentik/web": {
"resolved": "",

View File

@ -12,7 +12,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.4-1744139776",
"@goauthentik/api": "^2025.2.3-1743464496",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",

View File

@ -13,6 +13,7 @@ const MAX_PARAMS = 5;
// const MAX_COGNITIVE_COMPLEXITY = 9;
const rules = {
"no-param-reassign": "error",
"accessor-pairs": "error",
"array-callback-return": "error",
"block-scoped-var": "error",
@ -84,7 +85,6 @@ const rules = {
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-param-reassign": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
@ -134,6 +134,7 @@ const rules = {
// "sonarjs/cognitive-complexity": ["off", MAX_COGNITIVE_COMPLEXITY],
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
" @typescript-eslint/no-namespace": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",

View File

@ -48,6 +48,7 @@ export default [
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { BrandConfig, ServerConfig } from "@goauthentik/common/global";
import { globalAK } from "@goauthentik/common/global";
import "@goauthentik/elements/EmptyState";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
@ -33,7 +33,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
const version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
let build: string | TemplateResult = msg("Release");
if (ServerConfig.capabilities.includes(CapabilitiesEnum.CanDebug)) {
if (globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug)) {
build = msg("Development");
} else if (version.buildHash !== "") {
build = html`<a
@ -58,8 +58,8 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
}
renderModal() {
let product = BrandConfig.brandingTitle || DefaultBrand.brandingTitle;
if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle;
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`<div

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
import { MessageLevel } from "@goauthentik/common/messages";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
@ -55,12 +54,10 @@ export class DebugPage extends AKElement {
message: "Success",
});
})
.catch(async (error) => {
const parsedError = await parseAPIResponseError(error);
.catch((exc) => {
showMessage({
level: MessageLevel.error,
message: pluckErrorDetail(parsedError),
message: exc,
});
});
}}

View File

@ -1,16 +1,13 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { PFSize } from "@goauthentik/common/enums.js";
import {
APIError,
parseAPIResponseError,
pluckErrorDetail,
} from "@goauthentik/common/errors/network";
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
import { msg } from "@lit/localize";
import { PropertyValues, TemplateResult, html, nothing } from "lit";
import { state } from "lit/decorators.js";
import { ResponseError } from "@goauthentik/api";
export interface AdminStatus {
icon: string;
message?: TemplateResult;
@ -32,7 +29,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
// Current error state if any request fails
@state()
protected error?: APIError;
protected error?: string;
// Abstract methods to be implemented by subclasses
abstract getPrimaryValue(): Promise<T>;
@ -62,9 +59,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
this.value = value; // Triggers shouldUpdate
this.error = undefined;
})
.catch(async (error: unknown) => {
.catch((err: ResponseError) => {
this.status = undefined;
this.error = await parseAPIResponseError(error);
this.error = err?.response?.statusText ?? msg("Unknown error");
});
}
@ -82,9 +79,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
this.status = status;
this.error = undefined;
})
.catch(async (error: unknown) => {
.catch((err: ResponseError) => {
this.status = undefined;
this.error = await parseAPIResponseError(error);
this.error = err?.response?.statusText ?? msg("Unknown error");
});
// Prevent immediate re-render if only value changed
@ -123,8 +120,8 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
*/
private renderError(error: string): TemplateResult {
return html`
<p><i class="fa fa-times"></i>&nbsp;${msg("Failed to fetch")}</p>
<p class="subtext">${error}</p>
<p><i class="fa fa-times"></i>&nbsp;${error}</p>
<p class="subtext">${msg("Failed to fetch")}</p>
`;
}
@ -149,7 +146,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
this.status
? this.renderStatus(this.status) // Status available
: this.error
? this.renderError(pluckErrorDetail(this.error)) // Error state
? this.renderError(this.error) // Error state
: this.renderLoading() // Loading state
}
</p>

View File

@ -2,7 +2,7 @@ import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels";
import { formatElapsedTime } from "@goauthentik/common/temporal";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-event-info";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/Dropdown";
@ -10,7 +10,6 @@ import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import { SlottedTemplateResult } from "@goauthentik/elements/types";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
@ -69,12 +68,12 @@ export class RecentEventsCard extends Table<Event> {
</div>`;
}
row(item: EventWithContext): SlottedTemplateResult[] {
row(item: EventWithContext): TemplateResult[] {
return [
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
<small>${item.app}</small>`,
EventUser(item),
html`<div>${formatElapsedTime(item.created)}</div>
html`<div>${getRelativeTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`,
html` <div>${item.clientIp || msg("-")}</div>
<small>${EventGeo(item)}</small>`,
@ -82,11 +81,7 @@ export class RecentEventsCard extends Table<Event> {
];
}
renderEmpty(inner?: SlottedTemplateResult): TemplateResult {
if (this.error) {
return super.renderEmpty(inner);
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state header=${msg("No Events found.")}>
<div slot="body">${msg("No matching events could be found.")}</div>

View File

@ -46,7 +46,7 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
return;
}
const outpost = outposts.results[0];
outpost.config["authentik_host"] = window.location.origin;
outpost.config.authentik_host = window.location.origin;
await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUpdate({
uuid: outpost.pk,
outpostRequest: outpost,

View File

@ -28,16 +28,18 @@ export class WorkersStatusCard extends AdminStatusCard<Worker[]> {
icon: "fa fa-times-circle pf-m-danger",
message: html`${msg("No workers connected. Background tasks will not run.")}`,
});
} else if (value.filter((w) => !w.versionMatching).length > 0) {
}
if (value.filter((w) => !w.versionMatching).length > 0) {
return Promise.resolve<AdminStatus>({
icon: "fa fa-times-circle pf-m-danger",
message: html`${msg("Worker with incorrect version connected.")}`,
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
});
}
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
});
}
renderValue() {

View File

@ -127,7 +127,7 @@ export class SyncStatusChart extends AKChart<SummarizedSyncStatus[]> {
msg("LDAP Source"),
),
];
this.centerText = statuses.reduce((total, el) => (total += el.total), 0).toString();
this.centerText = statuses.reduce((total, el) => total + el.total, 0).toString();
return statuses;
}

View File

@ -6,26 +6,26 @@ import { html } from "lit";
import "../AdminSettingsFooterLinks.js";
describe("ak-admin-settings-footer-link", () => {
afterEach(async () => {
await browser.execute(async () => {
await document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if (document.body["_$litPart$"]) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body["_$litPart$"];
afterEach(() => {
return browser.execute(() => {
document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if ("_$litPart$" in document.body) {
delete document.body._$litPart$;
}
});
});
it("should render an empty control", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" });
});
it("should not be valid if just a name is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" });
@ -33,7 +33,7 @@ describe("ak-admin-settings-footer-link", () => {
it("should be valid if just a URL is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({
@ -44,7 +44,7 @@ describe("ak-admin-settings-footer-link", () => {
it("should be valid if both are filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
@ -56,7 +56,7 @@ describe("ak-admin-settings-footer-link", () => {
it("should not be valid if the URL is not valid", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("never://foo.com");
await expect(await link.getProperty("toJson")).toEqual({

View File

@ -79,7 +79,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
});
}
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["metaIcon"];
const icon = this.getFormFiles().metaIcon;
if (icon || this.clearIcon) {
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
slug: app.slug,
@ -117,7 +117,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
if (!(ev instanceof InputEvent) || !ev.target) {
return;
}
this.clearIcon = !!(ev.target as HTMLInputElement).checked;
this.clearIcon = Boolean((ev.target as HTMLInputElement).checked);
}
renderForm(): TemplateResult {

View File

@ -6,7 +6,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml";
import * as YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult } from "lit";
@ -31,9 +31,8 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
getSuccessMessage(): string {
if (this.instance?.pbmUuid) {
return msg("Successfully updated entitlement.");
} else {
return msg("Successfully created entitlement.");
}
return msg("Successfully created entitlement.");
}
static get styles(): CSSResult[] {
@ -49,11 +48,10 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
pbmUuid: this.instance.pbmUuid || "",
applicationEntitlementRequest: data,
});
} else {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
renderForm(): TemplateResult {

View File

@ -1,7 +1,7 @@
import { styles } from "@goauthentik/admin/applications/wizard/ApplicationWizardFormStepStyles.css.js";
import { WizardStep } from "@goauthentik/components/ak-wizard/WizardStep.js";
import {
NavigationEventInit,
NavigationUpdate,
WizardNavigationEvent,
WizardUpdateEvent,
} from "@goauthentik/components/ak-wizard/events";
@ -14,9 +14,9 @@ import { property, query } from "lit/decorators.js";
import { ValidationError } from "@goauthentik/api";
import {
ApplicationTransactionValidationError,
type ApplicationWizardState,
type ApplicationWizardStateUpdate,
ExtendedValidationError,
} from "./types";
export class ApplicationWizardStep extends WizardStep {
@ -48,7 +48,7 @@ export class ApplicationWizardStep extends WizardStep {
}
protected removeErrors(
keyToDelete: keyof ApplicationTransactionValidationError,
keyToDelete: keyof ExtendedValidationError,
): ValidationError | undefined {
if (!this.wizard.errors) {
return undefined;
@ -71,7 +71,7 @@ export class ApplicationWizardStep extends WizardStep {
public handleUpdate(
update?: ApplicationWizardStateUpdate,
destination?: string,
enable?: NavigationEventInit,
enable?: NavigationUpdate,
) {
// Inform ApplicationWizard of content state
if (update) {

View File

@ -15,7 +15,6 @@ import {
ProviderModelEnum,
ProxyMode,
ProxyProvider,
RACProvider,
RadiusProvider,
RedirectURI,
SAMLProvider,
@ -51,9 +50,8 @@ function renderRadiusOverview(rawProvider: OneOfProvider) {
]);
}
function renderRACOverview(rawProvider: OneOfProvider) {
// @ts-expect-error TS6133
const _provider = rawProvider as RACProvider;
function renderRACOverview(_rawProvider: OneOfProvider) {
// const _provider = rawProvider as RACProvider;
}
function formatRedirectUris(uris: RedirectURI[] = []) {

View File

@ -1,14 +1,13 @@
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { isSlug } from "@goauthentik/common/utils.js";
import { isSlug, isURLInput } from "@goauthentik/common/utils.js";
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -21,13 +20,25 @@ import { type ApplicationRequest } from "@goauthentik/api";
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
/**
* Plucks the specified keys from an object, trimming their values if they are strings.
*
* @template T - The type of the input object.
* @template K - The keys to be plucked from the input object.
*
* @param {T} input - The input object.
* @param {Array<K>} keys - The keys to be plucked from the input object.
*/
function trimMany<T extends object, K extends keyof T>(input: T, keys: Array<K>): Pick<T, K> {
const result: Partial<T> = {};
const trimMany = (o: KeyUnknown, vs: string[]) =>
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
for (const key of keys) {
const value = input[key];
result[key] = (typeof value === "string" ? value.trim() : value) as T[K];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isStr = (v: any): v is string => typeof v === "string";
return result as Pick<T, K>;
}
@customElement("ak-application-wizard-application-step")
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
@ -37,7 +48,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
errors = new Map<string, string>();
@query("form#applicationform")
form!: HTMLFormElement;
declare form: HTMLFormElement;
constructor() {
super();
@ -54,27 +65,34 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
}
get buttons(): WizardButton[] {
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
return [
// ---
{ kind: "next", destination: "provider-choice" },
{ kind: "cancel" },
];
}
get valid() {
this.errors = new Map();
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
if (values["name"] === "") {
const trimmed = trimMany((this.formValues || {}) as Partial<ApplicationRequest>, [
"name",
"slug",
"metaLaunchUrl",
]);
if (!trimmed.name) {
this.errors.set("name", msg("An application name is required"));
}
if (
!(
isStr(values["metaLaunchUrl"]) &&
(values["metaLaunchUrl"] === "" || URL.canParse(values["metaLaunchUrl"]))
)
) {
if (!isURLInput(trimmed.metaLaunchUrl)) {
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
}
if (!(isStr(values["slug"]) && values["slug"] !== "" && isSlug(values["slug"]))) {
if (!isSlug(trimmed.slug)) {
this.errors.set("slug", msg("Not a valid slug"));
}
return this.errors.size === 0;
}
@ -82,27 +100,39 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
if (button.kind === "next") {
if (!this.valid) {
this.handleEnabling({
disabled: ["provider-choice", "provider", "bindings", "submit"],
disabled: [
// ---
"provider-choice",
"provider",
"bindings",
"submit",
],
});
return;
}
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
let payload: ApplicationWizardStateUpdate = {
app: this.formValues,
errors: this.removeErrors("app"),
};
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
payload = {
...payload,
provider: { name: `Provider for ${app.name}` },
};
}
this.handleUpdate(payload, button.destination, {
enable: "provider-choice",
});
return;
}
super.handleButton(button);
}
@ -181,6 +211,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
if (!(this.wizard.app && this.wizard.errors)) {
throw new Error("Application Step received uninitialized wizard context.");
}
return this.renderForm(
this.wizard.app as ApplicationRequest,
this.wizard.errors?.app ?? {},

View File

@ -45,7 +45,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
hide = true;
@query("form#bindingform")
form!: HTMLFormElement;
declare form: HTMLFormElement;
@query(".policy-search-select")
searchSelect!: SearchSelectBase<Policy> | SearchSelectBase<Group> | SearchSelectBase<User>;

View File

@ -1,10 +1,9 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIResponseError } from "@goauthentik/common/errors/network";
import { parseAPIError } from "@goauthentik/common/errors";
import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
import { showAPIErrorMessage } from "@goauthentik/elements/messages/MessageContainer";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { P, match } from "ts-pattern";
@ -31,11 +30,10 @@ import {
type TransactionApplicationRequest,
type TransactionApplicationResponse,
type TransactionPolicyBindingRequest,
instanceOfValidationError,
} from "@goauthentik/api";
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
import { OneOfProvider, isApplicationTransactionValidationError } from "../types.js";
import { ExtendedValidationError, OneOfProvider } from "../types.js";
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
const _submitStates = ["reviewing", "running", "submitted"] as const;
@ -133,46 +131,39 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
this.state = "running";
return new CoreApi(DEFAULT_CONFIG)
.coreTransactionalApplicationsUpdate({
transactionApplicationRequest: request,
})
.then((_response: TransactionApplicationResponse) => {
this.dispatchCustomEvent(EVENT_REFRESH);
this.state = "submitted";
})
return (
new CoreApi(DEFAULT_CONFIG)
.coreTransactionalApplicationsUpdate({
transactionApplicationRequest: request,
})
.then((_response: TransactionApplicationResponse) => {
this.dispatchCustomEvent(EVENT_REFRESH);
this.state = "submitted";
})
.catch(async (error) => {
const parsedError = await parseAPIResponseError(error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => {
const errors = (await parseAPIError(
await resolution,
)) as ExtendedValidationError;
if (!instanceOfValidationError(parsedError)) {
showAPIErrorMessage(parsedError);
return;
}
if (isApplicationTransactionValidationError(parsedError)) {
// THIS is a really gross special case; if the user is duplicating the name of an existing provider, the error appears on the `app` (!) error object.
// We have to move that to the `provider.name` error field so it shows up in the right place.
if (Array.isArray(parsedError.app?.provider)) {
const providerError = parsedError.app.provider;
parsedError.provider = {
...parsedError.provider,
name: providerError,
};
delete parsedError.app.provider;
if (Object.keys(parsedError.app).length === 0) {
delete parsedError.app;
// THIS is a really gross special case; if the user is duplicating the name of
// an existing provider, the error appears on the `app` (!) error object. We
// have to move that to the `provider.name` error field so it shows up in the
// right place.
if (Array.isArray(errors?.app?.provider)) {
const providerError = errors.app.provider;
errors.provider = errors.provider ?? {};
errors.provider.name = providerError;
delete errors.app.provider;
if (Object.keys(errors.app).length === 0) {
delete errors.app;
}
}
}
this.handleUpdate({ errors: parsedError });
this.state = "reviewing";
});
this.handleUpdate({ errors });
this.state = "reviewing";
})
);
}
override handleButton(button: WizardButton) {
@ -234,20 +225,22 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
}
renderError() {
const { errors } = this.wizard;
if (Object.keys(errors).length === 0) return nothing;
if (Object.keys(this.wizard.errors).length === 0) {
return nothing;
}
const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step));
const errors = this.wizard.errors;
return html` <hr class="pf-c-divider" />
${match(errors)
${match(errors as ExtendedValidationError)
.with(
{ app: P.nonNullable },
() =>
html`<p>${msg("There was an error in the application.")}</p>
<p>
<a @click=${WizardNavigationEvent.toListener(this, "application")}>
${msg("Review the application.")}
</a>
<a @click=${navTo("application")}
>${msg("Review the application.")}</a
>
</p>`,
)
.with(
@ -255,20 +248,13 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
() =>
html`<p>${msg("There was an error in the provider.")}</p>
<p>
<a @click=${WizardNavigationEvent.toListener(this, "provider")}
>${msg("Review the provider.")}</a
>
<a @click=${navTo("provider")}>${msg("Review the provider.")}</a>
</p>`,
)
.with(
{ detail: P.nonNullable },
() =>
html`<p>
${msg(
"There was an error. Please go back and review the application.",
)}:
${errors.detail}
</p>`,
`<p>${msg("There was an error. Please go back and review the application.")}: ${errors.detail}</p>`,
)
.with(
{
@ -278,7 +264,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
html`<p>${msg("There was an error:")}:</p>
<ul>
${(errors.nonFieldErrors ?? []).map(
(reason) => html`<li>${reason}</li>`,
(e: string) => html`<li>${e}</li>`,
)}
</ul>
<p>${msg("Please go back and review the application.")}</p>`,

View File

@ -9,7 +9,7 @@ import { customElement, state } from "lit/decorators.js";
import { OAuth2ProviderRequest, SourcesApi } from "@goauthentik/api";
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
import { ApplicationTransactionValidationError } from "../../types.js";
import { ExtendedValidationError } from "../../types.js";
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
@customElement("ak-application-wizard-provider-for-oauth")
@ -34,7 +34,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid
});
}
renderForm(provider: OAuth2Provider, errors: ApplicationTransactionValidationError) {
renderForm(provider: OAuth2Provider, errors: ExtendedValidationError) {
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};

View File

@ -22,7 +22,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider
const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
this.hasSigningKp = Boolean(target.selectedKeypair);
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>

View File

@ -25,30 +25,16 @@ export type OneOfProvider =
export type ValidationRecord = { [key: string]: string[] };
/**
* An error that occurs during the creation or modification of an application.
*
* @todo (Elf) Extend this type to include all possible errors that can occur during the creation or modification of an application.
*/
export interface ApplicationTransactionValidationError extends ValidationError {
// TODO: Elf, extend this type and apply it to every object in the wizard. Then run
// the type-checker again.
export type ExtendedValidationError = ValidationError & {
app?: ValidationRecord;
provider?: ValidationRecord;
bindings?: ValidationRecord;
detail?: unknown;
}
/**
* Type-guard to determine if an API response is shaped like an {@linkcode ApplicationTransactionValidationError}.
*/
export function isApplicationTransactionValidationError(
error: ValidationError,
): error is ApplicationTransactionValidationError {
if ("app" in error) return true;
if ("provider" in error) return true;
if ("bindings" in error) return true;
return false;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
detail?: any;
};
// We use the PolicyBinding instead of the PolicyBindingRequest here, because that gives us a slot
// in which to preserve the retrieved policy, group, or user object from the SearchSelect used to
@ -63,7 +49,7 @@ export interface ApplicationWizardState {
proxyMode: ProxyMode;
bindings: PolicyBinding[];
currentBinding: number;
errors: ValidationError | ApplicationTransactionValidationError;
errors: ExtendedValidationError;
}
export interface ApplicationWizardStateUpdate {

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