Compare commits

..

1 Commits

Author SHA1 Message Date
19c5b28cb2 endpoints: initial data structure
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-28 23:22:38 +02:00
262 changed files with 985 additions and 2749 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2025.6.3 current_version = 2025.4.1
tag = True tag = True
commit = 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*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@ -21,8 +21,6 @@ optional_value = final
[bumpversion:file:package.json] [bumpversion:file:package.json]
[bumpversion:file:package-lock.json]
[bumpversion:file:docker-compose.yml] [bumpversion:file:docker-compose.yml]
[bumpversion:file:schema.yml] [bumpversion:file:schema.yml]
@ -33,4 +31,6 @@ optional_value = final
[bumpversion:file:internal/constants/constants.go] [bumpversion:file:internal/constants/constants.go]
[bumpversion:file:web/src/common/constants.ts]
[bumpversion:file:lifecycle/aws/template.yaml] [bumpversion:file:lifecycle/aws/template.yaml]

View File

@ -202,7 +202,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: web/dist path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
- name: prepare web ui - name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true' if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web working-directory: web

View File

@ -49,7 +49,6 @@ jobs:
matrix: matrix:
job: job:
- build - build
- build:integrations
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4

View File

@ -2,7 +2,7 @@ name: "CodeQL"
on: on:
push: push:
branches: [main, next, version*] branches: [main, "*", next, version*]
pull_request: pull_request:
branches: [main] branches: [main]
schedule: schedule:

View File

@ -96,7 +96,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
# Stage 5: Download uv # Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.8 AS uv FROM ghcr.io/astral-sh/uv:0.7.8 AS uv
# Stage 6: Base python image # Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \ ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \ PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \

View File

@ -86,10 +86,6 @@ dev-create-db:
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state. dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
update-test-mmdb: ## Update test GeoIP and ASN Databases
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
######################### #########################
## API Schema ## API Schema
######################### #########################

View File

@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
| Version | Supported | | Version | Supported |
| --------- | --------- | | --------- | --------- |
| 2025.2.x | ✅ |
| 2025.4.x | ✅ | | 2025.4.x | ✅ |
| 2025.6.x | ✅ |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2025.6.3" __version__ = "2025.4.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -148,14 +148,3 @@ class TestBrands(APITestCase):
"default_locale": "", "default_locale": "",
}, },
) )
def test_custom_css(self):
"""Test custom_css"""
brand = create_test_brand()
brand.branding_custom_css = """* {
font-family: "Foo bar";
}"""
brand.save()
res = self.client.get(reverse("authentik_core:if-user"))
self.assertEqual(res.status_code, 200)
self.assertIn(brand.branding_custom_css, res.content.decode())

View File

@ -5,8 +5,6 @@ from typing import Any
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models import Value as V from django.db.models import Value as V
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.utils.html import _json_script_escapes
from django.utils.safestring import mark_safe
from authentik import get_full_version from authentik import get_full_version
from authentik.brands.models import Brand from authentik.brands.models import Brand
@ -34,13 +32,8 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects brand object into every template""" """Context Processor that injects brand object into every template"""
brand = getattr(request, "brand", DEFAULT_BRAND) brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant()) tenant = getattr(request, "tenant", Tenant())
# similarly to `json_script` we escape everything HTML-related, however django
# only directly exposes this as a function that also wraps it in a <script> tag
# which we dont want for CSS
brand_css = mark_safe(str(brand.branding_custom_css).translate(_json_script_escapes)) # nosec
return { return {
"brand": brand, "brand": brand,
"brand_css": brand_css,
"footer_links": tenant.footer_links, "footer_links": tenant.footer_links,
"html_meta": {**get_http_meta()}, "html_meta": {**get_http_meta()},
"version": get_full_version(), "version": get_full_version(),

View File

@ -79,7 +79,6 @@ def _migrate_session(
AuthenticatedSession.objects.using(db_alias).create( AuthenticatedSession.objects.using(db_alias).create(
session=session, session=session,
user=old_auth_session.user, user=old_auth_session.user,
uuid=old_auth_session.uuid,
) )

View File

@ -1,81 +1,10 @@
# Generated by Django 5.1.9 on 2025-05-14 11:15 # Generated by Django 5.1.9 on 2025-05-14 11:15
from django.apps.registry import Apps, apps as global_apps from django.apps.registry import Apps
from django.db import migrations from django.db import migrations
from django.contrib.contenttypes.management import create_contenttypes
from django.contrib.auth.management import create_permissions
from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_authenticated_session_permissions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
"""Migrate permissions from OldAuthenticatedSession to AuthenticatedSession"""
db_alias = schema_editor.connection.alias
# `apps` here is just an instance of `django.db.migrations.state.AppConfigStub`, we need the
# real config for creating permissions and content types
authentik_core_config = global_apps.get_app_config("authentik_core")
# These are only ran by django after all migrations, but we need them right now.
# `global_apps` is needed,
create_permissions(authentik_core_config, using=db_alias, verbosity=1)
create_contenttypes(authentik_core_config, using=db_alias, verbosity=1)
# But from now on, this is just a regular migration, so use `apps`
Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model("contenttypes", "ContentType")
try:
old_ct = ContentType.objects.using(db_alias).get(
app_label="authentik_core", model="oldauthenticatedsession"
)
new_ct = ContentType.objects.using(db_alias).get(
app_label="authentik_core", model="authenticatedsession"
)
except ContentType.DoesNotExist:
# This should exist at this point, but if not, let's cut our losses
return
# Get all permissions for the old content type
old_perms = Permission.objects.using(db_alias).filter(content_type=old_ct)
# Create equivalent permissions for the new content type
for old_perm in old_perms:
new_perm = (
Permission.objects.using(db_alias)
.filter(
content_type=new_ct,
codename=old_perm.codename,
)
.first()
)
if not new_perm:
# This should exist at this point, but if not, let's cut our losses
continue
# Global user permissions
User = apps.get_model("authentik_core", "User")
User.user_permissions.through.objects.using(db_alias).filter(
permission=old_perm
).all().update(permission=new_perm)
# Global role permissions
DjangoGroup = apps.get_model("auth", "Group")
DjangoGroup.permissions.through.objects.using(db_alias).filter(
permission=old_perm
).all().update(permission=new_perm)
# Object user permissions
UserObjectPermission = apps.get_model("guardian", "UserObjectPermission")
UserObjectPermission.objects.using(db_alias).filter(permission=old_perm).all().update(
permission=new_perm, content_type=new_ct
)
# Object role permissions
GroupObjectPermission = apps.get_model("guardian", "GroupObjectPermission")
GroupObjectPermission.objects.using(db_alias).filter(permission=old_perm).all().update(
permission=new_perm, content_type=new_ct
)
def remove_old_authenticated_session_content_type( def remove_old_authenticated_session_content_type(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor apps: Apps, schema_editor: BaseDatabaseSchemaEditor
): ):
@ -92,12 +21,7 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython(
code=migrate_authenticated_session_permissions,
reverse_code=migrations.RunPython.noop,
),
migrations.RunPython( migrations.RunPython(
code=remove_old_authenticated_session_content_type, code=remove_old_authenticated_session_content_type,
reverse_code=migrations.RunPython.noop,
), ),
] ]

View File

@ -16,7 +16,7 @@
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<style>{{ brand_css }}</style> <style>{{ brand.branding_custom_css }}</style>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %} {% block head %}

View File

View File

@ -0,0 +1,12 @@
"""authentik endpoints app config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikEndpointsConfig(ManagedAppConfig):
"""authentik endpoints app config"""
name = "authentik.endpoints"
label = "authentik_endpoints"
verbose_name = "authentik Endpoints"
default = True

View File

@ -0,0 +1,47 @@
from enum import Enum
from pydantic import BaseModel
class UNSUPPORTED(BaseModel):
pass
class OSFamily(Enum):
linux = "linux"
unix = "unix"
bsd = "bsd"
windows = "windows"
macOS = "mac_os"
android = "android"
iOS = "i_os"
other = "other"
class CommonDeviceData(BaseModel):
class Disk(BaseModel):
encryption: bool
class OS(BaseModel):
firewall_enabled: bool
family: OSFamily
name: str
version: str
class Network(BaseModel):
hostname: str
dns_servers: list[str]
class Hardware(BaseModel):
model: str
manufacturer: str
class Software(BaseModel):
name: str
version: str
os: OS | UNSUPPORTED
disks: list[Disk] | UNSUPPORTED
network: Network | UNSUPPORTED
hardware: Hardware | UNSUPPORTED
software: list[Software] | UNSUPPORTED

View File

@ -0,0 +1,16 @@
from authentik.blueprints import models
class EnrollmentMethods(models.TextChoices):
AUTOMATIC_USER = "automatic_user" # Automatically enrolled through user action
AUTOMATIC_API = "automatic_api" # Automatically enrolled through connector integration
MANUAL_USER = "manual_user" # Manually enrolled
class BaseConnector:
def __init__(self) -> None:
pass
def supported_enrollment_methods(self) -> list[EnrollmentMethods]:
return []

View File

@ -0,0 +1,7 @@
from authentik.endpoints.connector import BaseConnector, EnrollmentMethods
class GoogleChromeConnector(BaseConnector):
def supported_enrollment_methods(self) -> list[EnrollmentMethods]:
return [EnrollmentMethods.AUTOMATIC_USER]

View File

@ -0,0 +1,7 @@
from django.db import models
from authentik.endpoints.models import Connector
class GoogleChromeConnector(Connector):
credentials = models.JSONField()

View File

@ -0,0 +1,125 @@
# Generated by Django 5.0.9 on 2024-09-24 19:16
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Connector",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("connector_uuid", models.UUIDField(default=uuid.uuid4)),
("name", models.TextField()),
(
"enrollment_method",
models.TextField(
choices=[
("automatic_user", "Automatic User"),
("automatic_api", "Automatic Api"),
("manual_user", "Manual User"),
]
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="Device",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("device_uuid", models.UUIDField(default=uuid.uuid4)),
("identifier", models.TextField(unique=True)),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="DeviceConnection",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("device_connection_uuid", models.UUIDField(default=uuid.uuid4)),
("data", models.JSONField(default=dict)),
(
"connection",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_endpoints.connector",
),
),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_endpoints.device"
),
),
],
),
migrations.AddField(
model_name="device",
name="connections",
field=models.ManyToManyField(
through="authentik_endpoints.DeviceConnection", to="authentik_endpoints.connector"
),
),
migrations.CreateModel(
name="DeviceUser",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("device_user_uuid", models.UUIDField(default=uuid.uuid4)),
("is_primary", models.BooleanField()),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_endpoints.device"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
),
migrations.AddField(
model_name="device",
name="users",
field=models.ManyToManyField(
through="authentik_endpoints.DeviceUser", to=settings.AUTH_USER_MODEL
),
),
]

View File

@ -0,0 +1,40 @@
from uuid import uuid4
from django.db import models
from django.utils.functional import cached_property
from authentik.core.models import User
from authentik.endpoints.common_data import CommonDeviceData
from authentik.lib.models import SerializerModel
class Device(SerializerModel):
device_uuid = models.UUIDField(default=uuid4)
identifier = models.TextField(unique=True)
users = models.ManyToManyField(User, through="DeviceUser")
connections = models.ManyToManyField("Connector", through="DeviceConnection")
@cached_property
def data(self) -> CommonDeviceData:
pass
class DeviceUser(models.Model):
device_user_uuid = models.UUIDField(default=uuid4)
device = models.ForeignKey("Device", on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
is_primary = models.BooleanField()
class DeviceConnection(models.Model):
device_connection_uuid = models.UUIDField(default=uuid4)
device = models.ForeignKey("Device", on_delete=models.CASCADE)
connection = models.ForeignKey("Connector", on_delete=models.CASCADE)
data = models.JSONField(default=dict)
class Connector(SerializerModel):
connector_uuid = models.UUIDField(default=uuid4)
name = models.TextField()

View File

@ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor):
self.reader: Reader | None = None self.reader: Reader | None = None
self._last_mtime: float = 0.0 self._last_mtime: float = 0.0
self.logger = get_logger() self.logger = get_logger()
self.load() self.open()
def path(self) -> str | None: def path(self) -> str | None:
"""Get the path to the MMDB file to load""" """Get the path to the MMDB file to load"""
raise NotImplementedError raise NotImplementedError
def load(self): def open(self):
"""Get GeoIP Reader, if configured, otherwise none""" """Get GeoIP Reader, if configured, otherwise none"""
path = self.path() path = self.path()
if path == "" or not path: if path == "" or not path:
@ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor):
diff = self._last_mtime < mtime diff = self._last_mtime < mtime
if diff > 0: if diff > 0:
self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path) self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
self.load() self.open()
except OSError as exc: except OSError as exc:
self.logger.warning("Failed to check MMDB age", exc=exc) self.logger.warning("Failed to check MMDB age", exc=exc)

View File

@ -81,6 +81,7 @@ debugger: false
log_level: info log_level: info
session_storage: cache
sessions: sessions:
unauthenticated_age: days=1 unauthenticated_age: days=1

View File

@ -130,7 +130,7 @@ class SyncTasks:
def sync_objects( def sync_objects(
self, object_type: str, page: int, provider_pk: int, override_dry_run=False, **filter self, object_type: str, page: int, provider_pk: int, override_dry_run=False, **filter
): ):
_object_type: type[Model] = path_to_class(object_type) _object_type = path_to_class(object_type)
self.logger = get_logger().bind( self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model), provider_type=class_to_path(self._provider_model),
provider_pk=provider_pk, provider_pk=provider_pk,
@ -156,11 +156,7 @@ class SyncTasks:
messages.append( messages.append(
asdict( asdict(
LogEvent( LogEvent(
_( _("Syncing page {page} of groups".format(page=page)),
"Syncing page {page} of {object_type}".format(
page=page, object_type=_object_type._meta.verbose_name_plural
)
),
log_level="info", log_level="info",
logger=f"{provider._meta.verbose_name}@{object_type}", logger=f"{provider._meta.verbose_name}@{object_type}",
) )

View File

@ -1,9 +1,11 @@
"""Websocket tests""" """Websocket tests"""
from dataclasses import asdict from dataclasses import asdict
from unittest.mock import patch
from channels.routing import URLRouter from channels.routing import URLRouter
from channels.testing import WebsocketCommunicator from channels.testing import WebsocketCommunicator
from django.contrib.contenttypes.models import ContentType
from django.test import TransactionTestCase from django.test import TransactionTestCase
from authentik import __version__ from authentik import __version__
@ -14,6 +16,12 @@ from authentik.providers.proxy.models import ProxyProvider
from authentik.root import websocket from authentik.root import websocket
def patched__get_ct_cached(app_label, codename):
"""Caches `ContentType` instances like its `QuerySet` does."""
return ContentType.objects.get(app_label=app_label, permission__codename=codename)
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestOutpostWS(TransactionTestCase): class TestOutpostWS(TransactionTestCase):
"""Websocket tests""" """Websocket tests"""

View File

@ -66,10 +66,7 @@ class RACClientConsumer(AsyncWebsocketConsumer):
def init_outpost_connection(self): def init_outpost_connection(self):
"""Initialize guac connection settings""" """Initialize guac connection settings"""
self.token = ( self.token = (
ConnectionToken.filter_not_expired( ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"])
token=self.scope["url_route"]["kwargs"]["token"],
session__session__session_key=self.scope["session"].session_key,
)
.select_related("endpoint", "provider", "session", "session__user") .select_related("endpoint", "provider", "session", "session__user")
.first() .first()
) )

View File

@ -166,6 +166,7 @@ class ConnectionToken(ExpiringModel):
always_merger.merge(settings, default_settings) always_merger.merge(settings, default_settings)
always_merger.merge(settings, self.endpoint.provider.settings) always_merger.merge(settings, self.endpoint.provider.settings)
always_merger.merge(settings, self.endpoint.settings) always_merger.merge(settings, self.endpoint.settings)
always_merger.merge(settings, self.settings)
def mapping_evaluator(mappings: QuerySet): def mapping_evaluator(mappings: QuerySet):
for mapping in mappings: for mapping in mappings:
@ -190,7 +191,6 @@ class ConnectionToken(ExpiringModel):
mapping_evaluator( mapping_evaluator(
RACPropertyMapping.objects.filter(endpoint__in=[self.endpoint]).order_by("name") RACPropertyMapping.objects.filter(endpoint__in=[self.endpoint]).order_by("name")
) )
always_merger.merge(settings, self.settings)
settings["drive-path"] = f"/tmp/connection/{self.token}" # nosec settings["drive-path"] = f"/tmp/connection/{self.token}" # nosec
settings["create-drive-path"] = "true" settings["create-drive-path"] = "true"

View File

@ -90,6 +90,23 @@ class TestModels(TransactionTestCase):
"resize-method": "display-update", "resize-method": "display-update",
}, },
) )
# Set settings in token
token.settings = {
"level": "token",
}
token.save()
self.assertEqual(
token.get_settings(),
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"level": "token",
"resize-method": "display-update",
},
)
# Set settings in property mapping (provider) # Set settings in property mapping (provider)
mapping = RACPropertyMapping.objects.create( mapping = RACPropertyMapping.objects.create(
name=generate_id(), name=generate_id(),
@ -134,22 +151,3 @@ class TestModels(TransactionTestCase):
"resize-method": "display-update", "resize-method": "display-update",
}, },
) )
# Set settings in token
token.settings = {
"level": "token",
}
token.save()
self.assertEqual(
token.get_settings(),
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"foo": "true",
"bar": "6",
"resize-method": "display-update",
"level": "token",
},
)

View File

@ -87,22 +87,3 @@ class TestRACViews(APITestCase):
) )
body = loads(flow_response.content) body = loads(flow_response.content)
self.assertEqual(body["component"], "ak-stage-access-denied") self.assertEqual(body["component"], "ak-stage-access-denied")
def test_different_session(self):
"""Test request"""
self.client.force_login(self.user)
response = self.client.get(
reverse(
"authentik_providers_rac:start",
kwargs={"app": self.app.slug, "endpoint": str(self.endpoint.pk)},
)
)
self.assertEqual(response.status_code, 302)
flow_response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
body = loads(flow_response.content)
next_url = body["to"]
self.client.logout()
final_response = self.client.get(next_url)
self.assertEqual(final_response.url, reverse("authentik_core:if-user"))

View File

@ -20,9 +20,6 @@ from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.views import PolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
PLAN_CONNECTION_SETTINGS = "connection_settings"
class RACStartView(PolicyAccessView): class RACStartView(PolicyAccessView):
@ -68,10 +65,7 @@ class RACInterface(InterfaceView):
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
# Early sanity check to ensure token still exists # Early sanity check to ensure token still exists
token = ConnectionToken.filter_not_expired( token = ConnectionToken.filter_not_expired(token=self.kwargs["token"]).first()
token=self.kwargs["token"],
session__session__session_key=request.session.session_key,
).first()
if not token: if not token:
return redirect("authentik_core:if-user") return redirect("authentik_core:if-user")
self.token = token self.token = token
@ -115,15 +109,10 @@ class RACFinalStage(RedirectStage):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_challenge(self, *args, **kwargs) -> RedirectChallenge: def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
settings = self.executor.plan.context.get(PLAN_CONNECTION_SETTINGS)
if not settings:
settings = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).get(
PLAN_CONNECTION_SETTINGS
)
token = ConnectionToken.objects.create( token = ConnectionToken.objects.create(
provider=self.provider, provider=self.provider,
endpoint=self.endpoint, endpoint=self.endpoint,
settings=settings or {}, settings=self.executor.plan.context.get("connection_settings", {}),
session=self.request.session["authenticatedsession"], session=self.request.session["authenticatedsession"],
expires=now() + timedelta_from_string(self.provider.connection_expiry), expires=now() + timedelta_from_string(self.provider.connection_expiry),
expiring=True, expiring=True,

View File

@ -47,16 +47,15 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema: def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema:
"""Convert authentik user into SCIM""" """Convert authentik user into SCIM"""
raw_scim_group = super().to_schema(obj, connection) raw_scim_group = super().to_schema(
obj,
connection,
schemas=(SCIM_GROUP_SCHEMA,),
)
try: try:
scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group)) scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group))
except ValidationError as exc: except ValidationError as exc:
raise StopSync(exc, obj) from exc raise StopSync(exc, obj) from exc
if SCIM_GROUP_SCHEMA not in scim_group.schemas:
scim_group.schemas.insert(0, SCIM_GROUP_SCHEMA)
# As this might be unset, we need to tell pydantic it's set so ensure the schemas
# are included, even if its just the defaults
scim_group.schemas = list(scim_group.schemas)
if not scim_group.externalId: if not scim_group.externalId:
scim_group.externalId = str(obj.pk) scim_group.externalId = str(obj.pk)

View File

@ -31,16 +31,15 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema: def to_schema(self, obj: User, connection: SCIMProviderUser) -> SCIMUserSchema:
"""Convert authentik user into SCIM""" """Convert authentik user into SCIM"""
raw_scim_user = super().to_schema(obj, connection) raw_scim_user = super().to_schema(
obj,
connection,
schemas=(SCIM_USER_SCHEMA,),
)
try: try:
scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user)) scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user))
except ValidationError as exc: except ValidationError as exc:
raise StopSync(exc, obj) from exc raise StopSync(exc, obj) from exc
if SCIM_USER_SCHEMA not in scim_user.schemas:
scim_user.schemas.insert(0, SCIM_USER_SCHEMA)
# As this might be unset, we need to tell pydantic it's set so ensure the schemas
# are included, even if its just the defaults
scim_user.schemas = list(scim_user.schemas)
if not scim_user.externalId: if not scim_user.externalId:
scim_user.externalId = str(obj.uid) scim_user.externalId = str(obj.uid)
return scim_user return scim_user

View File

@ -91,57 +91,6 @@ class SCIMUserTests(TestCase):
}, },
) )
@Mocker()
def test_user_create_custom_schema(self, mock: Mocker):
"""Test user creation with custom schema"""
schema = SCIMMapping.objects.create(
name="custom_schema",
expression="""return {"schemas": ["foo"]}""",
)
self.provider.property_mappings.add(schema)
scim_id = generate_id()
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
)
mock.post(
"https://localhost/Users",
json={
"id": scim_id,
},
)
uid = generate_id()
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 2)
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "foo"],
"active": True,
"emails": [
{
"primary": True,
"type": "other",
"value": f"{uid}@goauthentik.io",
}
],
"externalId": user.uid,
"name": {
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": f"{uid} {uid}",
"userName": uid,
},
)
@Mocker() @Mocker()
def test_user_create_different_provider_same_id(self, mock: Mocker): def test_user_create_different_provider_same_id(self, mock: Mocker):
"""Test user creation with multiple providers that happen """Test user creation with multiple providers that happen

View File

@ -73,6 +73,7 @@ TENANT_APPS = [
"authentik.admin", "authentik.admin",
"authentik.api", "authentik.api",
"authentik.crypto", "authentik.crypto",
"authentik.endpoints",
"authentik.flows", "authentik.flows",
"authentik.outposts", "authentik.outposts",
"authentik.policies.dummy", "authentik.policies.dummy",

View File

@ -3,46 +3,25 @@
import os import os
from argparse import ArgumentParser from argparse import ArgumentParser
from unittest import TestCase from unittest import TestCase
from unittest.mock import patch
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner
from structlog.stdlib import get_logger
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.sentry import sentry_init from authentik.lib.sentry import sentry_init
from authentik.root.signals import post_startup, pre_startup, startup from authentik.root.signals import post_startup, pre_startup, startup
from tests.e2e.utils import get_docker_tag
# globally set maxDiff to none to show full assert error # globally set maxDiff to none to show full assert error
TestCase.maxDiff = None TestCase.maxDiff = None
def get_docker_tag() -> str:
"""Get docker-tag based off of CI variables"""
env_pr_branch = "GITHUB_HEAD_REF"
default_branch = "GITHUB_REF"
branch_name = os.environ.get(default_branch, "main")
if os.environ.get(env_pr_branch, "") != "":
branch_name = os.environ[env_pr_branch]
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
return f"gh-{branch_name}"
def patched__get_ct_cached(app_label, codename):
"""Caches `ContentType` instances like its `QuerySet` does."""
return ContentType.objects.get(app_label=app_label, permission__codename=codename)
class PytestTestRunner(DiscoverRunner): # pragma: no cover class PytestTestRunner(DiscoverRunner): # pragma: no cover
"""Runs pytest to discover and run tests.""" """Runs pytest to discover and run tests."""
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.logger = get_logger().bind(runner="pytest")
self.args = [] self.args = []
if self.failfast: if self.failfast:
@ -69,10 +48,6 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
CONFIG.set("error_reporting.sample_rate", 0) CONFIG.set("error_reporting.sample_rate", 0)
CONFIG.set("error_reporting.environment", "testing") CONFIG.set("error_reporting.environment", "testing")
CONFIG.set("error_reporting.send_pii", True) CONFIG.set("error_reporting.send_pii", True)
ASN_CONTEXT_PROCESSOR.load()
GEOIP_CONTEXT_PROCESSOR.load()
sentry_init() sentry_init()
pre_startup.send(sender=self, mode="test") pre_startup.send(sender=self, mode="test")
@ -138,10 +113,4 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
f"path instead." f"path instead."
) )
self.logger.info("Running tests", test_files=self.args) return pytest.main(self.args)
with patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached):
try:
return pytest.main(self.args)
except Exception as e:
self.logger.error("Error running tests", error=str(e), test_files=self.args)
return 1

View File

@ -103,7 +103,6 @@ class LDAPSourceSerializer(SourceSerializer):
"user_object_filter", "user_object_filter",
"group_object_filter", "group_object_filter",
"group_membership_field", "group_membership_field",
"user_membership_attribute",
"object_uniqueness_field", "object_uniqueness_field",
"password_login_update_internal_password", "password_login_update_internal_password",
"sync_users", "sync_users",
@ -140,7 +139,6 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"user_object_filter", "user_object_filter",
"group_object_filter", "group_object_filter",
"group_membership_field", "group_membership_field",
"user_membership_attribute",
"object_uniqueness_field", "object_uniqueness_field",
"password_login_update_internal_password", "password_login_update_internal_password",
"sync_users", "sync_users",

View File

@ -1,32 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-29 11:22
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def set_user_membership_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
LDAPSource = apps.get_model("authentik_sources_ldap", "LDAPSource")
db_alias = schema_editor.connection.alias
LDAPSource.objects.using(db_alias).filter(group_membership_field="memberUid").all().update(
user_membership_attribute="ldap_uniq"
)
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_ldap", "0009_groupldapsourceconnection_validated_by_and_more"),
]
operations = [
migrations.AddField(
model_name="ldapsource",
name="user_membership_attribute",
field=models.TextField(
default="distinguishedName",
help_text="Attribute which matches the value of `group_membership_field`.",
),
),
migrations.RunPython(set_user_membership_attribute, migrations.RunPython.noop),
]

View File

@ -100,10 +100,6 @@ class LDAPSource(Source):
default="(objectClass=person)", default="(objectClass=person)",
help_text=_("Consider Objects matching this filter to be Users."), help_text=_("Consider Objects matching this filter to be Users."),
) )
user_membership_attribute = models.TextField(
default=LDAP_DISTINGUISHED_NAME,
help_text=_("Attribute which matches the value of `group_membership_field`."),
)
group_membership_field = models.TextField( group_membership_field = models.TextField(
default="member", help_text=_("Field which contains members of a group.") default="member", help_text=_("Field which contains members of a group.")
) )

View File

@ -71,11 +71,17 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
if not ak_group: if not ak_group:
continue continue
membership_mapping_attribute = LDAP_DISTINGUISHED_NAME
if self._source.group_membership_field == "memberUid":
# If memberships are based on the posixGroup's 'memberUid'
# attribute we use the RDN instead of the FDN to lookup members.
membership_mapping_attribute = LDAP_UNIQUENESS
users = User.objects.filter( users = User.objects.filter(
Q(**{f"attributes__{self._source.user_membership_attribute}__in": members}) Q(**{f"attributes__{membership_mapping_attribute}__in": members})
| Q( | Q(
**{ **{
f"attributes__{self._source.user_membership_attribute}__isnull": True, f"attributes__{membership_mapping_attribute}__isnull": True,
"ak_groups__in": [ak_group], "ak_groups__in": [ak_group],
} }
) )

View File

@ -71,31 +71,37 @@ def ldap_sync_single(source_pk: str):
return return
# Delete all sync tasks from the cache # Delete all sync tasks from the cache
DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete() DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
task = chain(
# The order of these operations needs to be preserved as each depends on the previous one(s) # User and group sync can happen at once, they have no dependencies on each other
# 1. User and group sync can happen simultaneously group(
# 2. Membership sync needs to run afterwards ldap_sync_paginator(source, UserLDAPSynchronizer)
# 3. Finally, user and group deletions can happen simultaneously + ldap_sync_paginator(source, GroupLDAPSynchronizer),
user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator( ),
source, GroupLDAPSynchronizer # Membership sync needs to run afterwards
group(
ldap_sync_paginator(source, MembershipLDAPSynchronizer),
),
# Finally, deletions. What we'd really like to do here is something like
# ```
# user_identifiers = <ldap query>
# User.objects.exclude(
# usersourceconnection__identifier__in=user_uniqueness_identifiers,
# ).delete()
# ```
# This runs into performance issues in large installations. So instead we spread the
# work out into three steps:
# 1. Get every object from the LDAP source.
# 2. Mark every object as "safe" in the database. This is quick, but any error could
# mean deleting users which should not be deleted, so we do it immediately, in
# large chunks, and only queue the deletion step afterwards.
# 3. Delete every unmarked item. This is slow, so we spread it over many tasks in
# small chunks.
group(
ldap_sync_paginator(source, UserLDAPForwardDeletion)
+ ldap_sync_paginator(source, GroupLDAPForwardDeletion),
),
) )
membership_sync = ldap_sync_paginator(source, MembershipLDAPSynchronizer) task()
user_group_deletion = ldap_sync_paginator(
source, UserLDAPForwardDeletion
) + ldap_sync_paginator(source, GroupLDAPForwardDeletion)
# Celery is buggy with empty groups, so we are careful only to add non-empty groups.
# See https://github.com/celery/celery/issues/9772
task_groups = []
if user_group_sync:
task_groups.append(group(user_group_sync))
if membership_sync:
task_groups.append(group(membership_sync))
if user_group_deletion:
task_groups.append(group(user_group_deletion))
all_tasks = chain(task_groups)
all_tasks()
def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list: def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list:

View File

@ -269,56 +269,12 @@ class LDAPSyncTests(TestCase):
self.source.group_membership_field = "memberUid" self.source.group_membership_field = "memberUid"
self.source.user_object_filter = "(objectClass=posixAccount)" self.source.user_object_filter = "(objectClass=posixAccount)"
self.source.group_object_filter = "(objectClass=posixGroup)" self.source.group_object_filter = "(objectClass=posixGroup)"
self.source.user_membership_attribute = "uid"
self.source.user_property_mappings.set( 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")
).all(),
LDAPSourcePropertyMapping.objects.create(
name="name",
expression='return {"attributes": {"uid": list_flatten(ldap.get("uid"))}}',
),
]
)
self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter( LDAPSourcePropertyMapping.objects.filter(
managed="goauthentik.io/sources/ldap/openldap-cn" Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
) )
) )
connection = MagicMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
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()
# Test if membership mapping based on memberUid works.
posix_group = Group.objects.filter(name="group-posix").first()
self.assertTrue(posix_group.users.filter(name="user-posix").exists())
def test_sync_groups_openldap_posix_group_nonstandard_membership_attribute(self):
"""Test posix group sync"""
self.source.object_uniqueness_field = "cn"
self.source.group_membership_field = "memberUid"
self.source.user_object_filter = "(objectClass=posixAccount)"
self.source.group_object_filter = "(objectClass=posixGroup)"
self.source.user_membership_attribute = "cn"
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")
).all(),
LDAPSourcePropertyMapping.objects.create(
name="name",
expression='return {"attributes": {"cn": list_flatten(ldap.get("cn"))}}',
),
]
)
self.source.group_property_mappings.set( self.source.group_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter( LDAPSourcePropertyMapping.objects.filter(
managed="goauthentik.io/sources/ldap/openldap-cn" managed="goauthentik.io/sources/ldap/openldap-cn"

View File

@ -151,7 +151,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
webauthn_user_verification=UserVerification.PREFERRED, webauthn_user_verification=UserVerification.PREFERRED,
) )
stage.webauthn_allowed_device_types.set( stage.webauthn_allowed_device_types.set(
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") WebAuthnDeviceType.objects.filter(
description="Android Authenticator with SafetyNet Attestation"
)
) )
session = self.client.session session = self.client.session
plan = FlowPlan(flow_pk=flow.pk.hex) plan = FlowPlan(flow_pk=flow.pk.hex)
@ -337,7 +339,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
device_classes=[DeviceClasses.WEBAUTHN], device_classes=[DeviceClasses.WEBAUTHN],
) )
stage.webauthn_allowed_device_types.set( stage.webauthn_allowed_device_types.set(
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") WebAuthnDeviceType.objects.filter(
description="Android Authenticator with SafetyNet Attestation"
)
) )
session = self.client.session session = self.client.session
plan = FlowPlan(flow_pk=flow.pk.hex) plan = FlowPlan(flow_pk=flow.pk.hex)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -141,7 +141,9 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
"""Test registration with restricted devices (fail)""" """Test registration with restricted devices (fail)"""
webauthn_mds_import.delay(force=True).get() webauthn_mds_import.delay(force=True).get()
self.stage.device_type_restrictions.set( self.stage.device_type_restrictions.set(
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series") WebAuthnDeviceType.objects.filter(
description="Android Authenticator with SafetyNet Attestation"
)
) )
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

View File

@ -100,11 +100,9 @@ def send_mail(
# Because we use the Message-ID as UID for the task, manually assign it # Because we use the Message-ID as UID for the task, manually assign it
message_object.extra_headers["Message-ID"] = message_id message_object.extra_headers["Message-ID"] = message_id
# Add the logo if it is used in the email body (we can't add it in the # Add the logo (we can't add it in the previous message since MIMEImage
# previous message since MIMEImage can't be converted to json) # can't be converted to json)
body = get_email_body(message_object) message_object.attach(logo_data())
if "cid:logo" in body:
message_object.attach(logo_data())
if ( if (
message_object.to message_object.to

View File

@ -96,7 +96,7 @@
<table width="100%" style="background-color: #FFFFFF; border-spacing: 0; margin-top: 15px;"> <table width="100%" style="background-color: #FFFFFF; border-spacing: 0; margin-top: 15px;">
<tr height="80"> <tr height="80">
<td align="center" style="padding: 20px 0;"> <td align="center" style="padding: 20px 0;">
<img src="{% block logo_url %}cid:logo{% endblock %}" border="0=" alt="authentik logo" class="flexibleImage logo"> <img src="{% block logo_url %}cid:logo.png{% endblock %}" border="0=" alt="authentik logo" class="flexibleImage logo">
</td> </td>
</tr> </tr>
{% block content %} {% block content %}

View File

@ -19,8 +19,7 @@ def logo_data() -> MIMEImage:
path = Path("web/dist/assets/icons/icon_left_brand.png") path = Path("web/dist/assets/icons/icon_left_brand.png")
with open(path, "rb") as _logo_file: with open(path, "rb") as _logo_file:
logo = MIMEImage(_logo_file.read()) logo = MIMEImage(_logo_file.read())
logo.add_header("Content-ID", "<logo>") logo.add_header("Content-ID", "logo.png")
logo.add_header("Content-Disposition", "inline", filename="logo.png")
return logo return logo

View File

@ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware):
SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
) )
if configured_binding_net != NetworkBinding.NO_BINDING: if configured_binding_net != NetworkBinding.NO_BINDING:
BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip) self.recheck_session_net(configured_binding_net, last_ip, new_ip)
if configured_binding_geo != GeoIPBinding.NO_BINDING: if configured_binding_geo != GeoIPBinding.NO_BINDING:
BoundSessionMiddleware.recheck_session_geo(configured_binding_geo, last_ip, new_ip) self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
# If we got to this point without any error being raised, we need to # If we got to this point without any error being raised, we need to
# update the last saved IP to the current one # update the last saved IP to the current one
if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session: if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session:
@ -111,8 +111,7 @@ class BoundSessionMiddleware(SessionMiddleware):
# (== basically requires the user to be logged in) # (== basically requires the user to be logged in)
request.session[request.session.model.Keys.LAST_IP] = new_ip request.session[request.session.model.Keys.LAST_IP] = new_ip
@staticmethod def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
def recheck_session_net(binding: NetworkBinding, last_ip: str, new_ip: str):
"""Check network/ASN binding""" """Check network/ASN binding"""
last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip) last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip) new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
@ -159,8 +158,7 @@ class BoundSessionMiddleware(SessionMiddleware):
new_ip, new_ip,
) )
@staticmethod def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
def recheck_session_geo(binding: GeoIPBinding, last_ip: str, new_ip: str):
"""Check GeoIP binding""" """Check GeoIP binding"""
last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip) last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip) new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
@ -181,8 +179,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.continent != new_geo.continent: if last_geo.continent != new_geo.continent:
raise SessionBindingBroken( raise SessionBindingBroken(
"geoip.continent", "geoip.continent",
last_geo.continent.to_dict(), last_geo.continent,
new_geo.continent.to_dict(), new_geo.continent,
last_ip, last_ip,
new_ip, new_ip,
) )
@ -194,8 +192,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.country != new_geo.country: if last_geo.country != new_geo.country:
raise SessionBindingBroken( raise SessionBindingBroken(
"geoip.country", "geoip.country",
last_geo.country.to_dict(), last_geo.country,
new_geo.country.to_dict(), new_geo.country,
last_ip, last_ip,
new_ip, new_ip,
) )
@ -204,8 +202,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.city != new_geo.city: if last_geo.city != new_geo.city:
raise SessionBindingBroken( raise SessionBindingBroken(
"geoip.city", "geoip.city",
last_geo.city.to_dict(), last_geo.city,
new_geo.city.to_dict(), new_geo.city,
last_ip, last_ip,
new_ip, new_ip,
) )

View File

@ -11,7 +11,7 @@ from rest_framework.fields import BooleanField, CharField
from authentik.core.models import Session, User from authentik.core.models import Session, User
from authentik.events.middleware import audit_ignore from authentik.events.middleware import audit_ignore
from authentik.flows.challenge import ChallengeResponse, WithUserInfoChallenge from authentik.flows.challenge import ChallengeResponse, WithUserInfoChallenge
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.root.middleware import ClientIPMiddleware from authentik.root.middleware import ClientIPMiddleware
@ -108,6 +108,10 @@ class UserLoginStageView(ChallengeStageView):
flow_slug=self.executor.flow.slug, flow_slug=self.executor.flow.slug,
session_duration=delta, session_duration=delta,
) )
# Only show success message if we don't have a source in the flow
# as sources show their own success messages
if not self.executor.plan.context.get(PLAN_CONTEXT_SOURCE, None):
messages.success(self.request, _("Successfully logged in!"))
if self.executor.current_stage.terminate_other_sessions: if self.executor.current_stage.terminate_other_sessions:
Session.objects.filter( Session.objects.filter(
authenticatedsession__user=user, authenticatedsession__user=user,

View File

@ -3,7 +3,6 @@
from time import sleep from time import sleep
from unittest.mock import patch from unittest.mock import patch
from django.http import HttpRequest
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
@ -18,12 +17,7 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.root.middleware import ClientIPMiddleware from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.user_login.middleware import ( from authentik.stages.user_login.models import UserLoginStage
BoundSessionMiddleware,
SessionBindingBroken,
logout_extra,
)
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage
class TestUserLoginStage(FlowTestCase): class TestUserLoginStage(FlowTestCase):
@ -198,52 +192,3 @@ class TestUserLoginStage(FlowTestCase):
self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
response = self.client.get(reverse("authentik_api:application-list")) response = self.client.get(reverse("authentik_api:application-list"))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_binding_net_break_log(self):
"""Test logout_extra with exception"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json
for args, expect in [
[[NetworkBinding.BIND_ASN, "8.8.8.8", "8.8.8.8"], ["network.missing"]],
[[NetworkBinding.BIND_ASN, "1.0.0.1", "1.128.0.1"], ["network.asn"]],
[
[NetworkBinding.BIND_ASN_NETWORK, "12.81.96.1", "12.81.128.1"],
["network.asn_network"],
],
[[NetworkBinding.BIND_ASN_NETWORK_IP, "1.0.0.1", "1.0.0.2"], ["network.ip"]],
]:
with self.subTest(args[0]):
with self.assertRaises(SessionBindingBroken) as cm:
BoundSessionMiddleware.recheck_session_net(*args)
self.assertEqual(cm.exception.reason, expect[0])
# Ensure the request can be logged without throwing errors
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
request.user = self.user
logout_extra(request, cm.exception)
def test_binding_geo_break_log(self):
"""Test logout_extra with exception"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
for args, expect in [
[[GeoIPBinding.BIND_CONTINENT, "8.8.8.8", "8.8.8.8"], ["geoip.missing"]],
[[GeoIPBinding.BIND_CONTINENT, "2.125.160.216", "67.43.156.1"], ["geoip.continent"]],
[
[GeoIPBinding.BIND_CONTINENT_COUNTRY, "81.2.69.142", "89.160.20.112"],
["geoip.country"],
],
[
[GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY, "2.125.160.216", "81.2.69.142"],
["geoip.city"],
],
]:
with self.subTest(args[0]):
with self.assertRaises(SessionBindingBroken) as cm:
BoundSessionMiddleware.recheck_session_geo(*args)
self.assertEqual(cm.exception.reason, expect[0])
# Ensure the request can be logged without throwing errors
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
request.user = self.user
logout_extra(request, cm.exception)

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2025.6.3 Blueprint schema", "title": "authentik 2025.4.1 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"
@ -8147,12 +8147,6 @@
"title": "Group membership field", "title": "Group membership field",
"description": "Field which contains members of a group." "description": "Field which contains members of a group."
}, },
"user_membership_attribute": {
"type": "string",
"minLength": 1,
"title": "User membership attribute",
"description": "Attribute which matches the value of `group_membership_field`."
},
"object_uniqueness_field": { "object_uniqueness_field": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,

View File

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

2
go.mod
View File

@ -27,7 +27,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025041.4 goauthentik.io/api/v3 v3.2025041.2
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.14.0 golang.org/x/sync v0.14.0

4
go.sum
View File

@ -290,8 +290,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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025041.4 h1:cGqzWYnUHrWDoaXWDpIL/kWnX9sFrIhkYDye0P0OEAo= goauthentik.io/api/v3 v3.2025041.2 h1:vFYYnhcDcxL95RczZwhzt3i4LptFXMvIRN+vgf8sQYg=
goauthentik.io/api/v3 v3.2025041.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2025041.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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

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

View File

@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/gob" "encoding/gob"
"encoding/hex"
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
@ -119,8 +118,8 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
mux := mux.NewRouter() mux := mux.NewRouter()
// Save cookie name, based on hashed client ID // Save cookie name, based on hashed client ID
hs := sha256.Sum256([]byte(*p.ClientId)) h := sha256.New()
bs := hex.EncodeToString(hs[:]) bs := string(h.Sum([]byte(*p.ClientId)))
sessionName := fmt.Sprintf("authentik_proxy_%s", bs[:8]) sessionName := fmt.Sprintf("authentik_proxy_%s", bs[:8])
// When HOST_BROWSER is set, use that as Host header for token requests to make the issuer match // When HOST_BROWSER is set, use that as Host header for token requests to make the issuer match

View File

@ -3,7 +3,6 @@ package application
type ProxyClaims struct { type ProxyClaims struct {
UserAttributes map[string]interface{} `json:"user_attributes"` UserAttributes map[string]interface{} `json:"user_attributes"`
BackendOverride string `json:"backend_override"` BackendOverride string `json:"backend_override"`
HostHeader string `json:"host_header"`
IsSuperuser bool `json:"is_superuser"` IsSuperuser bool `json:"is_superuser"`
} }

View File

@ -74,18 +74,13 @@ func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
r.URL.Scheme = ou.Scheme r.URL.Scheme = ou.Scheme
r.URL.Host = ou.Host r.URL.Host = ou.Host
claims := a.getClaimsFromSession(r) claims := a.getClaimsFromSession(r)
if claims != nil && claims.Proxy != nil { if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" {
if claims.Proxy.BackendOverride != "" { u, err := url.Parse(claims.Proxy.BackendOverride)
u, err := url.Parse(claims.Proxy.BackendOverride) if err != nil {
if err != nil { a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override") } else {
} else { r.URL.Scheme = u.Scheme
r.URL.Scheme = u.Scheme r.URL.Host = u.Host
r.URL.Host = u.Host
}
}
if claims.Proxy.HostHeader != "" {
r.Host = claims.Proxy.HostHeader
} }
} }
a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url") a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url")

View File

@ -2,7 +2,6 @@ package radius
import ( import (
"crypto/sha512" "crypto/sha512"
"encoding/hex"
"time" "time"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
@ -69,9 +68,7 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
} }
} }
if pi == nil { if pi == nil {
hs := sha512.Sum512([]byte(r.Secret)) nr.Log().WithField("hashed_secret", string(sha512.New().Sum(r.Secret))).Warning("No provider found")
bs := hex.EncodeToString(hs[:])
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
_ = w.Write(r.Response(radius.CodeAccessReject)) _ = w.Write(r.Response(radius.CodeAccessReject))
return return
} }

View File

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

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-02 00:12+0000\n" "POT-Creation-Date: 2025-05-28 11:25+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -2226,10 +2226,6 @@ msgstr ""
msgid "Consider Objects matching this filter to be Users." msgid "Consider Objects matching this filter to be Users."
msgstr "" msgstr ""
#: authentik/sources/ldap/models.py
msgid "Attribute which matches the value of `group_membership_field`."
msgstr ""
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "Field which contains members of a group." msgid "Field which contains members of a group."
msgstr "" msgstr ""
@ -3497,6 +3493,10 @@ msgstr ""
msgid "No Pending user to login." msgid "No Pending user to login."
msgstr "" msgstr ""
#: authentik/stages/user_login/stage.py
msgid "Successfully logged in!"
msgstr ""
#: authentik/stages/user_logout/models.py #: authentik/stages/user_logout/models.py
msgid "User Logout Stage" msgid "User Logout Stage"
msgstr "" msgstr ""

View File

@ -975,11 +975,11 @@ msgstr "开始全量提供程序同步"
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing users" msgid "Syncing users"
msgstr "正在同步用户" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
msgid "Syncing groups" msgid "Syncing groups"
msgstr "正在同步组" msgstr ""
#: authentik/lib/sync/outgoing/tasks.py #: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format #, python-brace-format
@ -2291,7 +2291,7 @@ msgstr "基于用户属性而非组属性查询组成员身份。这允许在 Fr
msgid "" msgid ""
"Delete authentik users and groups which were previously supplied by this " "Delete authentik users and groups which were previously supplied by this "
"source, but are now missing from it." "source, but are now missing from it."
msgstr "删除之前由此源提供,但现已缺失的用户和组。" msgstr ""
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "LDAP Source" msgid "LDAP Source"
@ -2312,7 +2312,7 @@ msgstr "LDAP 源属性映射"
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "" msgid ""
"Unique ID used while checking if this object still exists in the directory." "Unique ID used while checking if this object still exists in the directory."
msgstr "检查此对象是否仍在目录中时使用的唯一 ID。" msgstr ""
#: authentik/sources/ldap/models.py #: authentik/sources/ldap/models.py
msgid "User LDAP Source Connection" msgid "User LDAP Source Connection"
@ -2694,7 +2694,7 @@ msgstr "组 SAML 源连接"
#: authentik/sources/saml/views.py #: authentik/sources/saml/views.py
#, python-brace-format #, python-brace-format
msgid "Continue to {source_name}" msgid "Continue to {source_name}"
msgstr "继续前往 {source_name}" msgstr ""
#: authentik/sources/scim/models.py #: authentik/sources/scim/models.py
msgid "SCIM Source" msgid "SCIM Source"
@ -3064,7 +3064,7 @@ msgstr "用户同意授权"
#: authentik/stages/consent/stage.py #: authentik/stages/consent/stage.py
msgid "Invalid consent token, re-showing prompt" msgid "Invalid consent token, re-showing prompt"
msgstr "无效的同意令牌,将重新显示输入" msgstr ""
#: authentik/stages/deny/models.py #: authentik/stages/deny/models.py
msgid "Deny Stage" msgid "Deny Stage"
@ -3084,11 +3084,11 @@ msgstr "虚拟阶段"
#: authentik/stages/email/flow.py #: authentik/stages/email/flow.py
msgid "Continue to confirm this email address." msgid "Continue to confirm this email address."
msgstr "继续以确认电子邮件地址。" msgstr ""
#: authentik/stages/email/flow.py #: authentik/stages/email/flow.py
msgid "Link was already used, please request a new link." msgid "Link was already used, please request a new link."
msgstr "链接已被使用,请申请一个新链接。" msgstr ""
#: authentik/stages/email/models.py #: authentik/stages/email/models.py
msgid "Password Reset" msgid "Password Reset"

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2025.6.3", "version": "2025.4.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2025.6.3", "version": "2025.4.1",
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.3.3", "prettier": "^3.3.3",

View File

@ -1,6 +1,6 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2025.6.3", "version": "2025.4.1",
"private": true, "private": true,
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
[project] [project]
name = "authentik" name = "authentik"
version = "2025.6.3" version = "2025.4.1"
description = "" description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*" requires-python = "==3.13.*"
@ -13,7 +13,7 @@ dependencies = [
"dacite==1.9.2", "dacite==1.9.2",
"deepmerge==2.0", "deepmerge==2.0",
"defusedxml==0.7.1", "defusedxml==0.7.1",
"django==5.1.11", "django==5.1.9",
"django-countries==7.6.1", "django-countries==7.6.1",
"django-cte==1.3.3", "django-cte==1.3.3",
"django-filter==25.1", "django-filter==25.1",
@ -61,7 +61,7 @@ dependencies = [
"setproctitle==1.3.6", "setproctitle==1.3.6",
"structlog==25.3.0", "structlog==25.3.0",
"swagger-spec-validator==3.0.4", "swagger-spec-validator==3.0.4",
"tenant-schemas-celery==3.0.0", "tenant-schemas-celery==4.0.1",
"twilio==9.6.1", "twilio==9.6.1",
"ua-parser==1.0.1", "ua-parser==1.0.1",
"unidecode==1.4.0", "unidecode==1.4.0",

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2025.6.3 version: 2025.4.1
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io
@ -28581,10 +28581,6 @@ paths:
name: sync_users_password name: sync_users_password
schema: schema:
type: boolean type: boolean
- in: query
name: user_membership_attribute
schema:
type: string
- in: query - in: query
name: user_object_filter name: user_object_filter
schema: schema:
@ -47897,9 +47893,6 @@ components:
group_membership_field: group_membership_field:
type: string type: string
description: Field which contains members of a group. description: Field which contains members of a group.
user_membership_attribute:
type: string
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field: object_uniqueness_field:
type: string type: string
description: Field which contains a unique Identifier. description: Field which contains a unique Identifier.
@ -48113,10 +48106,6 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Field which contains members of a group. description: Field which contains members of a group.
user_membership_attribute:
type: string
minLength: 1
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field: object_uniqueness_field:
type: string type: string
minLength: 1 minLength: 1
@ -53454,10 +53443,6 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Field which contains members of a group. description: Field which contains members of a group.
user_membership_attribute:
type: string
minLength: 1
description: Attribute which matches the value of `group_membership_field`.
object_uniqueness_field: object_uniqueness_field:
type: string type: string
minLength: 1 minLength: 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -2,6 +2,7 @@
from dataclasses import asdict from dataclasses import asdict
from time import sleep from time import sleep
from unittest.mock import patch
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
@ -15,10 +16,12 @@ from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
from authentik.outposts.tests.test_ws import patched__get_ct_cached
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider from authentik.providers.ldap.models import APIAccessMode, LDAPProvider
from tests.e2e.utils import SeleniumTestCase, retry from tests.e2e.utils import SeleniumTestCase, retry
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestProviderLDAP(SeleniumTestCase): class TestProviderLDAP(SeleniumTestCase):
"""LDAP and Outpost e2e tests""" """LDAP and Outpost e2e tests"""

View File

@ -6,6 +6,7 @@ from json import loads
from sys import platform from sys import platform
from time import sleep from time import sleep
from unittest.case import skip, skipUnless from unittest.case import skip, skipUnless
from unittest.mock import patch
from channels.testing import ChannelsLiveServerTestCase from channels.testing import ChannelsLiveServerTestCase
from jwt import decode from jwt import decode
@ -17,10 +18,12 @@ from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
from authentik.outposts.tasks import outpost_connection_discovery from authentik.outposts.tasks import outpost_connection_discovery
from authentik.outposts.tests.test_ws import patched__get_ct_cached
from authentik.providers.proxy.models import ProxyProvider from authentik.providers.proxy.models import ProxyProvider
from tests.e2e.utils import SeleniumTestCase, retry from tests.e2e.utils import SeleniumTestCase, retry
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestProviderProxy(SeleniumTestCase): class TestProviderProxy(SeleniumTestCase):
"""Proxy and Outpost e2e tests""" """Proxy and Outpost e2e tests"""

View File

@ -4,6 +4,7 @@ from json import loads
from pathlib import Path from pathlib import Path
from time import sleep from time import sleep
from unittest import skip from unittest import skip
from unittest.mock import patch
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
@ -12,10 +13,12 @@ from authentik.core.models import Application
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.outposts.models import Outpost, OutpostType from authentik.outposts.models import Outpost, OutpostType
from authentik.outposts.tests.test_ws import patched__get_ct_cached
from authentik.providers.proxy.models import ProxyMode, ProxyProvider from authentik.providers.proxy.models import ProxyMode, ProxyProvider
from tests.e2e.utils import SeleniumTestCase, retry from tests.e2e.utils import SeleniumTestCase, retry
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestProviderProxyForward(SeleniumTestCase): class TestProviderProxyForward(SeleniumTestCase):
"""Proxy and Outpost e2e tests""" """Proxy and Outpost e2e tests"""

View File

@ -2,6 +2,7 @@
from dataclasses import asdict from dataclasses import asdict
from time import sleep from time import sleep
from unittest.mock import patch
from pyrad.client import Client from pyrad.client import Client
from pyrad.dictionary import Dictionary from pyrad.dictionary import Dictionary
@ -12,10 +13,12 @@ from authentik.core.models import Application, User
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id, generate_key from authentik.lib.generators import generate_id, generate_key
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
from authentik.outposts.tests.test_ws import patched__get_ct_cached
from authentik.providers.radius.models import RadiusProvider from authentik.providers.radius.models import RadiusProvider
from tests.e2e.utils import SeleniumTestCase, retry from tests.e2e.utils import SeleniumTestCase, retry
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestProviderRadius(SeleniumTestCase): class TestProviderRadius(SeleniumTestCase):
"""Radius Outpost e2e tests""" """Radius Outpost e2e tests"""

View File

@ -1,6 +1,7 @@
"""authentik e2e testing utilities""" """authentik e2e testing utilities"""
import json import json
import os
import socket import socket
from collections.abc import Callable from collections.abc import Callable
from functools import lru_cache, wraps from functools import lru_cache, wraps
@ -36,12 +37,22 @@ from authentik.core.api.users import UserSerializer
from authentik.core.models import User from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.root.test_runner import get_docker_tag
IS_CI = "CI" in environ IS_CI = "CI" in environ
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1 RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
def get_docker_tag() -> str:
"""Get docker-tag based off of CI variables"""
env_pr_branch = "GITHUB_HEAD_REF"
default_branch = "GITHUB_REF"
branch_name = os.environ.get(default_branch, "main")
if os.environ.get(env_pr_branch, "") != "":
branch_name = os.environ[env_pr_branch]
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
return f"gh-{branch_name}"
def get_local_ip() -> str: def get_local_ip() -> str:
"""Get the local machine's IP""" """Get the local machine's IP"""
hostname = socket.gethostname() hostname = socket.gethostname()

71
uv.lock generated
View File

@ -164,7 +164,7 @@ wheels = [
[[package]] [[package]]
name = "authentik" name = "authentik"
version = "2025.6.3" version = "2025.4.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "argon2-cffi" }, { name = "argon2-cffi" },
@ -273,7 +273,7 @@ requires-dist = [
{ name = "dacite", specifier = "==1.9.2" }, { name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" }, { name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" }, { name = "defusedxml", specifier = "==0.7.1" },
{ name = "django", specifier = "==5.1.11" }, { name = "django", specifier = "==5.1.9" },
{ name = "django-countries", specifier = "==7.6.1" }, { name = "django-countries", specifier = "==7.6.1" },
{ name = "django-cte", specifier = "==1.3.3" }, { name = "django-cte", specifier = "==1.3.3" },
{ name = "django-filter", specifier = "==25.1" }, { name = "django-filter", specifier = "==25.1" },
@ -321,7 +321,7 @@ requires-dist = [
{ name = "setproctitle", specifier = "==1.3.6" }, { name = "setproctitle", specifier = "==1.3.6" },
{ name = "structlog", specifier = "==25.3.0" }, { name = "structlog", specifier = "==25.3.0" },
{ name = "swagger-spec-validator", specifier = "==3.0.4" }, { name = "swagger-spec-validator", specifier = "==3.0.4" },
{ name = "tenant-schemas-celery", specifier = "==3.0.0" }, { name = "tenant-schemas-celery", specifier = "==4.0.1" },
{ name = "twilio", specifier = "==9.6.1" }, { name = "twilio", specifier = "==9.6.1" },
{ name = "ua-parser", specifier = "==1.0.1" }, { name = "ua-parser", specifier = "==1.0.1" },
{ name = "unidecode", specifier = "==1.4.0" }, { name = "unidecode", specifier = "==1.4.0" },
@ -979,16 +979,16 @@ wheels = [
[[package]] [[package]]
name = "django" name = "django"
version = "5.1.11" version = "5.1.9"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" }, { name = "asgiref" },
{ name = "sqlparse" }, { name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/83/80/bf0f9b0aa434fca2b46fc6a31c39b08ea714b87a0a72a16566f053fb05a8/django-5.1.11.tar.gz", hash = "sha256:3bcdbd40e4d4623b5e04f59c28834323f3086df583058e65ebce99f9982385ce", size = 10734926, upload-time = "2025-06-10T10:12:48.229Z" } sdist = { url = "https://files.pythonhosted.org/packages/10/08/2e6f05494b3fc0a3c53736846034f882b82ee6351791a7815bbb45715d79/django-5.1.9.tar.gz", hash = "sha256:565881bdd0eb67da36442e9ac788bda90275386b549070d70aee86327781a4fc", size = 10710887, upload-time = "2025-05-07T14:06:45.257Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/59/91/2972ce330c6c0bd5b3200d4c2ad5cbf47eecff5243220c5a56444d3267a0/django-5.1.11-py3-none-any.whl", hash = "sha256:e48091f364007068728aca938e7450fbfe3f2217079bfd2b8af45122585acf64", size = 8277453, upload-time = "2025-06-10T10:12:42.236Z" }, { url = "https://files.pythonhosted.org/packages/e1/d1/d8b6b8250b84380d5a123e099ad3298a49407d81598faa13b43a2c6d96d7/django-5.1.9-py3-none-any.whl", hash = "sha256:2fd1d4a0a66a5ba702699eb692e75b0d828b73cc2f4e1fc4b6a854a918967411", size = 8277363, upload-time = "2025-05-07T14:06:37.426Z" },
] ]
[[package]] [[package]]
@ -2383,16 +2383,16 @@ wheels = [
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "6.31.1" version = "6.30.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } sdist = { url = "https://files.pythonhosted.org/packages/c8/8c/cf2ac658216eebe49eaedf1e06bc06cbf6a143469236294a1171a51357c3/protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048", size = 429315, upload-time = "2025-03-26T19:12:57.394Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, { url = "https://files.pythonhosted.org/packages/be/85/cd53abe6a6cbf2e0029243d6ae5fb4335da2996f6c177bb2ce685068e43d/protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103", size = 419148, upload-time = "2025-03-26T19:12:41.359Z" },
{ url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, { url = "https://files.pythonhosted.org/packages/97/e9/7b9f1b259d509aef2b833c29a1f3c39185e2bf21c9c1be1cd11c22cb2149/protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9", size = 431003, upload-time = "2025-03-26T19:12:44.156Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" }, { url = "https://files.pythonhosted.org/packages/8e/66/7f3b121f59097c93267e7f497f10e52ced7161b38295137a12a266b6c149/protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b", size = 417579, upload-time = "2025-03-26T19:12:45.447Z" },
{ url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, { url = "https://files.pythonhosted.org/packages/d0/89/bbb1bff09600e662ad5b384420ad92de61cab2ed0f12ace1fd081fd4c295/protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815", size = 317319, upload-time = "2025-03-26T19:12:46.999Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, { url = "https://files.pythonhosted.org/packages/28/50/1925de813499546bc8ab3ae857e3ec84efe7d2f19b34529d0c7c3d02d11d/protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d", size = 316212, upload-time = "2025-03-26T19:12:48.458Z" },
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, { url = "https://files.pythonhosted.org/packages/e5/a1/93c2acf4ade3c5b557d02d500b06798f4ed2c176fa03e3c34973ca92df7f/protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51", size = 167062, upload-time = "2025-03-26T19:12:55.892Z" },
] ]
[[package]] [[package]]
@ -2776,7 +2776,7 @@ wheels = [
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.4" version = "2.32.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@ -2784,9 +2784,9 @@ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
] ]
[[package]] [[package]]
@ -3100,33 +3100,32 @@ wheels = [
[[package]] [[package]]
name = "tenant-schemas-celery" name = "tenant-schemas-celery"
version = "3.0.0" version = "4.0.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "celery" }, { name = "celery" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d0/fe/cfe19eb7cc3ad8e39d7df7b7c44414bf665b6ac6660c998eb498f89d16c6/tenant_schemas_celery-3.0.0.tar.gz", hash = "sha256:6be3ae1a5826f262f0f3dd343c6a85a34a1c59b89e04ae37de018f36562fed55", size = 15954, upload-time = "2024-05-19T11:16:41.837Z" } sdist = { url = "https://files.pythonhosted.org/packages/19/f8/cf055bf171b5d83d6fe96f1840fba90d3d274be2b5c35cd21b873302b128/tenant_schemas_celery-4.0.1.tar.gz", hash = "sha256:8b8f055fcd82aa53274c09faf88653a935241518d93b86ab2d43a3df3b70c7f8", size = 18870, upload-time = "2025-04-22T18:23:51.061Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/2c/376e1e641ad08b374c75d896468a7be2e6906ce3621fd0c9f9dc09ff1963/tenant_schemas_celery-3.0.0-py3-none-any.whl", hash = "sha256:ca0f69e78ef698eb4813468231df5a0ab6a660c08e657b65f5ac92e16887eec8", size = 18108, upload-time = "2024-05-19T11:16:39.92Z" }, { url = "https://files.pythonhosted.org/packages/e9/a8/fd663c461550d6fedfb24e987acc1557ae5b6615ca08fc6c70dbaaa88aa5/tenant_schemas_celery-4.0.1-py3-none-any.whl", hash = "sha256:d06a3ff6956db3a95168ce2051b7bff2765f9ce0d070e14df92f07a2b60ae0a0", size = 21364, upload-time = "2025-04-22T18:23:49.899Z" },
] ]
[[package]] [[package]]
name = "tornado" name = "tornado"
version = "6.5.1" version = "6.4.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" } sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135, upload-time = "2024-11-22T03:06:38.036Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" }, { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299, upload-time = "2024-11-22T03:06:20.162Z" },
{ url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" }, { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253, upload-time = "2024-11-22T03:06:22.39Z" },
{ url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" }, { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602, upload-time = "2024-11-22T03:06:24.214Z" },
{ url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" }, { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972, upload-time = "2024-11-22T03:06:25.559Z" },
{ url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" }, { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173, upload-time = "2024-11-22T03:06:27.584Z" },
{ url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" }, { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892, upload-time = "2024-11-22T03:06:28.933Z" },
{ url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" }, { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334, upload-time = "2024-11-22T03:06:30.428Z" },
{ url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" }, { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261, upload-time = "2024-11-22T03:06:32.458Z" },
{ url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" }, { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463, upload-time = "2024-11-22T03:06:34.71Z" },
{ url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" }, { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907, upload-time = "2024-11-22T03:06:36.71Z" },
{ url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" },
] ]
[[package]] [[package]]
@ -3288,11 +3287,11 @@ wheels = [
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.5.0" version = "2.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]

8
web/package-lock.json generated
View File

@ -22,7 +22,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11", "@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.1-1748622869", "@goauthentik/api": "^2025.4.1-1748431399",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
@ -1721,9 +1721,9 @@
} }
}, },
"node_modules/@goauthentik/api": { "node_modules/@goauthentik/api": {
"version": "2025.4.1-1748622869", "version": "2025.4.1-1748431399",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.4.1-1748622869.tgz", "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2025.4.1-1748431399.tgz",
"integrity": "sha512-nH7+dQVA5yPoR4x0g3mct+M9VCwkBh/7ginUTwzb9O+Fj7HHGeAk/4xFC7Zy1oc6CIOHZbSMrOM5EdkEKE18Og==" "integrity": "sha512-j4ygP36DXqzBCFi3v8KWFLPLUmV616POZb8zx35RaCskuZ5BFNDaArLDtGHvCkEV3qJouR2w43hD4cX18BFIQw=="
}, },
"node_modules/@goauthentik/core": { "node_modules/@goauthentik/core": {
"resolved": "packages/core", "resolved": "packages/core",

View File

@ -93,7 +93,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11", "@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.1-1748622869", "@goauthentik/api": "^2025.4.1-1748431399",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",

View File

@ -85,8 +85,8 @@ export class AdminOverviewPage extends AdminOverviewBase {
render(): TemplateResult { render(): TemplateResult {
const username = this.user?.user.name || this.user?.user.username; const username = this.user?.user.name || this.user?.user.username;
return html`<ak-page-header return html` <ak-page-header
header=${this.user ? msg(str`Welcome, ${username || ""}.`) : msg("Welcome.")} header=${msg(str`Welcome, ${username || ""}.`)}
description=${msg("General system status")} description=${msg("General system status")}
?hasIcon=${false} ?hasIcon=${false}
> >

View File

@ -1,38 +1,26 @@
import { DEFAULT_CONFIG } from "#common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import {
DataProvision,
DualSelectPair,
DualSelectPairSource,
} from "#elements/ak-dual-select/types";
import { CertificateKeyPair, CryptoApi } from "@goauthentik/api"; import { CertificateKeyPair, CryptoApi } from "@goauthentik/api";
const certToSelect = (cert: CertificateKeyPair): DualSelectPair<CertificateKeyPair> => { const certToSelect = (s: CertificateKeyPair) => [s.pk, s.name, s.name, s];
return [cert.pk, cert.name, cert.name, cert];
};
export async function certificateProvider(page = 1, search = ""): Promise<DataProvision> { export async function certificateProvider(page = 1, search = "") {
return new CryptoApi(DEFAULT_CONFIG) const certificates = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({
.cryptoCertificatekeypairsList({ ordering: "name",
ordering: "name", pageSize: 20,
pageSize: 20, search: search.trim(),
search: search.trim(), page,
page, hasKey: undefined,
hasKey: undefined, });
}) return {
.then(({ pagination, results }) => { pagination: certificates.pagination,
return { options: certificates.results.map(certToSelect),
pagination, };
options: results.map(certToSelect),
};
});
} }
export function certificateSelector( export function certificateSelector(instanceMappings?: string[]) {
instanceMappings?: string[],
): DualSelectPairSource<CertificateKeyPair> {
if (!instanceMappings) { if (!instanceMappings) {
return () => Promise.resolve([]); return [];
} }
return async () => { return async () => {

View File

@ -361,7 +361,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<p class="pf-c-form__helper-text">${placeholderHelperText}</p> <p class="pf-c-form__helper-text">${placeholderHelperText}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Additional User DN")} label=${msg("Addition User DN")}
name="additionalUserDn" name="additionalUserDn"
> >
<input <input
@ -374,7 +374,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Additional Group DN")} label=${msg("Addition Group DN")}
name="additionalGroupDn" name="additionalGroupDn"
> >
<input <input
@ -429,25 +429,10 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
/> />
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"Field which contains members of a group. The value of this field is matched against User membership attribute.", "Field which contains members of a group. Note that if using the \"memberUid\" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.",
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("User membership attribute")}
?required=${true}
name="userMembershipAttribute"
>
<input
type="text"
value="${this.instance?.userMembershipAttribute || "distinguishedName"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Attribute which matches the value of Group membership field.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="lookupGroupsFromUser"> <ak-form-element-horizontal name="lookupGroupsFromUser">
<label class="pf-c-switch"> <label class="pf-c-switch">
<input <input

View File

@ -201,10 +201,6 @@ select[multiple] option:checked {
--pf-c-input-group--BackgroundColor: transparent; --pf-c-input-group--BackgroundColor: transparent;
} }
select.pf-c-form-control {
--pf-c-form-control__select--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%23fafafa' d='M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z'/%3E%3C/svg%3E");
}
.pf-c-form-control { .pf-c-form-control {
--pf-c-form-control--BorderTopColor: transparent !important; --pf-c-form-control--BorderTopColor: transparent !important;
--pf-c-form-control--BorderRightColor: transparent !important; --pf-c-form-control--BorderRightColor: transparent !important;

View File

@ -374,7 +374,7 @@ ${JSON.stringify(value.new_value, null, 4)}</pre
renderEmailSent() { renderEmailSent() {
let body = this.event.context.body as string; let body = this.event.context.body as string;
body = body.replace("cid:logo", "/static/dist/assets/icons/icon_left_brand.png"); body = body.replace("cid:logo.png", "/static/dist/assets/icons/icon_left_brand.png");
return html`<div class="pf-c-card__title">${msg("Email info:")}</div> return html`<div class="pf-c-card__title">${msg("Email info:")}</div>
<div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div> <div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div>
<ak-expand> <ak-expand>

View File

@ -147,7 +147,7 @@ export class AKPageNavbar
} }
.accent-icon { .accent-icon {
height: 1.2em; height: 1em;
width: 1em; width: 1em;
@media (max-width: 768px) { @media (max-width: 768px) {
@ -157,7 +157,6 @@ export class AKPageNavbar
} }
&.page-description { &.page-description {
padding-top: 0.3em;
grid-area: description; grid-area: description;
margin-block-end: var(--pf-global--spacer--md); margin-block-end: var(--pf-global--spacer--md);

View File

@ -123,9 +123,6 @@ export class AKElement extends LitElement {
applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet); applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet);
this.activeTheme = UiThemeEnum.Dark; this.activeTheme = UiThemeEnum.Dark;
} else if (this.preferredColorScheme === "light") {
applyUITheme(nextStyleRoot, UiThemeEnum.Light, this.#customCSSStyleSheet);
this.activeTheme = UiThemeEnum.Light;
} else if (this.preferredColorScheme === "auto") { } else if (this.preferredColorScheme === "auto") {
createUIThemeEffect( createUIThemeEffect(
(nextUITheme) => { (nextUITheme) => {

View File

@ -4,7 +4,7 @@ import { ref } from "lit/directives/ref.js";
import { AkDualSelectProvider } from "./ak-dual-select-provider.js"; import { AkDualSelectProvider } from "./ak-dual-select-provider.js";
import "./ak-dual-select.js"; import "./ak-dual-select.js";
import type { DualSelectPair, DualSelectPairSource } from "./types.js"; import type { DualSelectPair } from "./types.js";
/** /**
* @element ak-dual-select-dynamic-provider * @element ak-dual-select-dynamic-provider
@ -22,7 +22,7 @@ export class AkDualSelectDynamic extends AkDualSelectProvider {
* @attr * @attr
*/ */
@property({ attribute: false }) @property({ attribute: false })
selector?: DualSelectPairSource; selector: (_: DualSelectPair[]) => Promise<DualSelectPair[]> = () => Promise.resolve([]);
#didFirstUpdate = false; #didFirstUpdate = false;
@ -37,7 +37,7 @@ export class AkDualSelectDynamic extends AkDualSelectProvider {
this.#didFirstUpdate = true; this.#didFirstUpdate = true;
this.selector?.(this.options).then((selected) => { this.selector(this.options).then((selected) => {
this.selected = selected; this.selected = selected;
}); });
} }

View File

@ -32,8 +32,8 @@ import {
} from "./types.js"; } from "./types.js";
function localeComparator(a: DualSelectPair, b: DualSelectPair) { function localeComparator(a: DualSelectPair, b: DualSelectPair) {
const aSortBy = String(a[2] || a[0]); const aSortBy = a[2];
const bSortBy = String(b[2] || b[0]); const bSortBy = b[2];
return aSortBy.localeCompare(bSortBy); return aSortBy.localeCompare(bSortBy);
} }

View File

@ -34,7 +34,7 @@ export type DualSelectPair<T = unknown> = [
/** /**
* A string to sort by. If not provided, the key will be used. * A string to sort by. If not provided, the key will be used.
*/ */
sortBy?: string, sortBy: string,
/** /**
* A local mapping of the key to the object. This is used by some specific apps. * A local mapping of the key to the object. This is used by some specific apps.
* *
@ -43,19 +43,15 @@ export type DualSelectPair<T = unknown> = [
localMapping?: T, localMapping?: T,
]; ];
export type DualSelectPairSource<T = unknown> = (
sourceInit: DualSelectPair<T>[],
) => Promise<DualSelectPair<T>[]>;
export type BasePagination = Pick< export type BasePagination = Pick<
Pagination, Pagination,
"startIndex" | "endIndex" | "count" | "previous" | "next" "startIndex" | "endIndex" | "count" | "previous" | "next"
>; >;
export interface DataProvision<T = unknown> { export type DataProvision = {
pagination?: Pagination; pagination?: Pagination;
options: DualSelectPair<T>[]; options: DualSelectPair[];
} };
export type DataProvider = (page: number, search?: string) => Promise<DataProvision>; export type DataProvider = (page: number, search?: string) => Promise<DataProvision>;

View File

@ -11,7 +11,7 @@ import { StageHost } from "#flow/stages/base";
import "#user/user-settings/details/stages/prompt/PromptStage"; import "#user/user-settings/details/stages/prompt/PromptStage";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js";
@ -36,7 +36,7 @@ export class UserSettingsFlowExecutor
implements StageHost implements StageHost
{ {
@property() @property()
flowSlug = this.brand?.flowUserSettings; flowSlug?: string;
private _challenge?: ChallengeTypes; private _challenge?: ChallengeTypes;
@ -86,15 +86,12 @@ export class UserSettingsFlowExecutor
}); });
} }
firstUpdated() { updated(changedProperties: PropertyValues<this>): void {
if (this.flowSlug) { if (changedProperties.has("brand") && this.brand) {
this.nextChallenge();
}
}
updated(): void {
if (!this.flowSlug && this.brand?.flowUserSettings) {
this.flowSlug = this.brand.flowUserSettings; this.flowSlug = this.brand.flowUserSettings;
if (!this.flowSlug) return;
this.nextChallenge(); this.nextChallenge();
} }
} }

View File

@ -9106,6 +9106,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019"> <trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source> <source>Failed to preview prompt</source>
</trans-unit> </trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e"> <trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source> <source>Lookup using user attribute</source>
</trans-unit> </trans-unit>
@ -9243,18 +9246,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="se3b26b762110bda0"> <trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source> <source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -7608,6 +7608,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019"> <trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source> <source>Failed to preview prompt</source>
</trans-unit> </trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e"> <trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source> <source>Lookup using user attribute</source>
</trans-unit> </trans-unit>
@ -7745,18 +7748,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="se3b26b762110bda0"> <trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source> <source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9167,6 +9167,9 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
<trans-unit id="sc7524ea24eeeb019"> <trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source> <source>Failed to preview prompt</source>
</trans-unit> </trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e"> <trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source> <source>Lookup using user attribute</source>
</trans-unit> </trans-unit>
@ -9304,18 +9307,6 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
</trans-unit> </trans-unit>
<trans-unit id="se3b26b762110bda0"> <trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source> <source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9690,6 +9690,10 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<source>Failed to preview prompt</source> <source>Failed to preview prompt</source>
<target>Échec de la prévisualisation de l'invite</target> <target>Échec de la prévisualisation de l'invite</target>
</trans-unit> </trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
<target>Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...'. Lorsque "Recherche avec un attribut utilisateur" est sélectionné, cet attribut doit être un attribut utilisateur, sinon un attribut de groupe.</target>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e"> <trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source> <source>Lookup using user attribute</source>
<target>Recherche avec un attribut utilisateur</target> <target>Recherche avec un attribut utilisateur</target>
@ -9873,18 +9877,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="se3b26b762110bda0"> <trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source> <source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
<target>Supprimer les utilisateurs et les groupes authentik qui étaient auparavant fournis par cette source, mais qui en sont maintenant absents.</target> <target>Supprimer les utilisateurs et les groupes authentik qui étaient auparavant fournis par cette source, mais qui en sont maintenant absents.</target>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9690,6 +9690,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Failed to preview prompt</source> <source>Failed to preview prompt</source>
<target>Impossibile visualizzare l'anteprima del prompt</target> <target>Impossibile visualizzare l'anteprima del prompt</target>
</trans-unit> </trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
<target>Campo che contiene i membri di un gruppo. Si noti che se si utilizza il campo "memberUid", si presume che il valore contenga un nome relativo distinto. Ad esempio, "memberUid=some-user" invece di "memberUid=cn=some-user,ou=groups,...". Quando si seleziona "Cerca utilizzando un attributo utente", questo dovrebbe essere un attributo utente, altrimenti un attributo di gruppo.</target>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e"> <trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source> <source>Lookup using user attribute</source>
<target>Ricerca tramite attributo utente</target> <target>Ricerca tramite attributo utente</target>
@ -9856,18 +9860,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="se3b26b762110bda0"> <trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source> <source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9075,6 +9075,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019"> <trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source> <source>Failed to preview prompt</source>
</trans-unit> </trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e"> <trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source> <source>Lookup using user attribute</source>
</trans-unit> </trans-unit>
@ -9212,18 +9215,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="se3b26b762110bda0"> <trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source> <source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -8977,6 +8977,9 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="sc7524ea24eeeb019"> <trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source> <source>Failed to preview prompt</source>
</trans-unit> </trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e"> <trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source> <source>Lookup using user attribute</source>
</trans-unit> </trans-unit>
@ -9114,18 +9117,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit> </trans-unit>
<trans-unit id="se3b26b762110bda0"> <trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source> <source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9402,6 +9402,9 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<trans-unit id="sc7524ea24eeeb019"> <trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source> <source>Failed to preview prompt</source>
</trans-unit> </trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e"> <trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source> <source>Lookup using user attribute</source>
</trans-unit> </trans-unit>
@ -9539,18 +9542,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit> </trans-unit>
<trans-unit id="se3b26b762110bda0"> <trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source> <source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

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