mirror of
https://github.com/goauthentik/authentik
synced 2026-04-26 01:25:02 +02:00
Compare commits
84 Commits
admin/vers
...
port-captc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59bf2b143e | ||
|
|
19980d05e8 | ||
|
|
b53c004496 | ||
|
|
23ffad1c6b | ||
|
|
ce3f9e3763 | ||
|
|
4b0402b51a | ||
|
|
974bd6665d | ||
|
|
1cff6fdd67 | ||
|
|
70845b1d5c | ||
|
|
7a39d8ce64 | ||
|
|
12682bf08e | ||
|
|
86f430b7b1 | ||
|
|
f57d345c9a | ||
|
|
41e2106d9f | ||
|
|
467a26faf5 | ||
|
|
1525a4c2e5 | ||
|
|
9645bdc9b9 | ||
|
|
952edb75fd | ||
|
|
4b73a1ec38 | ||
|
|
600052bd0f | ||
|
|
5d5fb39736 | ||
|
|
cc7b25d828 | ||
|
|
a8134e26c8 | ||
|
|
1c637bc0d1 | ||
|
|
8af668a9d2 | ||
|
|
96266e2e2b | ||
|
|
65373ab217 | ||
|
|
88590e1134 | ||
|
|
f6ff31e3de | ||
|
|
cbc14524b3 | ||
|
|
d807038d05 | ||
|
|
ba131a50ba | ||
|
|
0e37a66751 | ||
|
|
57c220dfc2 | ||
|
|
7339a22080 | ||
|
|
8c2d72affe | ||
|
|
9c31e08bd9 | ||
|
|
805b9afa80 | ||
|
|
bc809bae1e | ||
|
|
66773b69ab | ||
|
|
c149701501 | ||
|
|
b13b51b73a | ||
|
|
ee30cc1ede | ||
|
|
de095f4a10 | ||
|
|
abf3aa3a7f | ||
|
|
3ebae72a76 | ||
|
|
b6b47b669e | ||
|
|
8422568c42 | ||
|
|
9973064f50 | ||
|
|
4fea65f5cc | ||
|
|
784446a47d | ||
|
|
516bc65fc4 | ||
|
|
efb59adeff | ||
|
|
43a2ad66f0 | ||
|
|
ec0c59f1fc | ||
|
|
8f80072321 | ||
|
|
33f95c837b | ||
|
|
43637b8a75 | ||
|
|
7a4518be26 | ||
|
|
b94fb53821 | ||
|
|
2be5c9633b | ||
|
|
e729e42595 | ||
|
|
01d591b84e | ||
|
|
dd08e1bf66 | ||
|
|
150705f221 | ||
|
|
6b39f6495e | ||
|
|
639c57245b | ||
|
|
730600aea4 | ||
|
|
e15ce5a3f0 | ||
|
|
1fc91b004b | ||
|
|
644705e6fe | ||
|
|
ff8ef523db | ||
|
|
1051dd19ea | ||
|
|
04cb4fd267 | ||
|
|
da9508f839 | ||
|
|
841a286a25 | ||
|
|
63c48d7b99 | ||
|
|
5994fd2c61 | ||
|
|
5f745e682e | ||
|
|
6f1b16e7f9 | ||
|
|
57bce19e7a | ||
|
|
850c5d5a45 | ||
|
|
8b7d11f94c | ||
|
|
45737909f6 |
@@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.4.1
|
||||
current_version = 2025.6.4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
@@ -21,6 +21,8 @@ optional_value = final
|
||||
|
||||
[bumpversion:file:package.json]
|
||||
|
||||
[bumpversion:file:package-lock.json]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
[bumpversion:file:schema.yml]
|
||||
@@ -31,6 +33,4 @@ optional_value = final
|
||||
|
||||
[bumpversion:file:internal/constants/constants.go]
|
||||
|
||||
[bumpversion:file:web/src/common/constants.ts]
|
||||
|
||||
[bumpversion:file:lifecycle/aws/template.yaml]
|
||||
|
||||
2
.github/workflows/ci-main.yml
vendored
2
.github/workflows/ci-main.yml
vendored
@@ -202,7 +202,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
|
||||
- name: prepare web ui
|
||||
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||
working-directory: web
|
||||
|
||||
1
.github/workflows/ci-website.yml
vendored
1
.github/workflows/ci-website.yml
vendored
@@ -49,6 +49,7 @@ jobs:
|
||||
matrix:
|
||||
job:
|
||||
- build
|
||||
- build:integrations
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -2,7 +2,7 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, "*", next, version*]
|
||||
branches: [main, next, version*]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
|
||||
@@ -96,7 +96,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
# Stage 5: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.8 AS uv
|
||||
# Stage 6: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
|
||||
4
Makefile
4
Makefile
@@ -86,6 +86,10 @@ dev-create-db:
|
||||
|
||||
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
|
||||
#########################
|
||||
|
||||
@@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | --------- |
|
||||
| 2025.2.x | ✅ |
|
||||
| 2025.4.x | ✅ |
|
||||
| 2025.6.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.4.1"
|
||||
__version__ = "2025.6.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from collections.abc import Callable
|
||||
from django.apps import apps
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.v1.importer import is_model_allowed
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.providers.oauth2.models import RefreshToken
|
||||
|
||||
@@ -22,10 +21,13 @@ def serializer_tester_factory(test_model: type[SerializerModel]) -> Callable:
|
||||
return
|
||||
model_class = test_model()
|
||||
self.assertTrue(isinstance(model_class, SerializerModel))
|
||||
# Models that have subclasses don't have to have a serializer
|
||||
if len(test_model.__subclasses__()) > 0:
|
||||
return
|
||||
self.assertIsNotNone(model_class.serializer)
|
||||
if model_class.serializer.Meta().model == RefreshToken:
|
||||
return
|
||||
self.assertEqual(model_class.serializer.Meta().model, test_model)
|
||||
self.assertTrue(issubclass(test_model, model_class.serializer.Meta().model))
|
||||
|
||||
return tester
|
||||
|
||||
@@ -34,6 +36,6 @@ for app in apps.get_app_configs():
|
||||
if not app.label.startswith("authentik"):
|
||||
continue
|
||||
for model in app.get_models():
|
||||
if not is_model_allowed(model):
|
||||
if not issubclass(model, SerializerModel):
|
||||
continue
|
||||
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
||||
|
||||
@@ -148,3 +148,14 @@ class TestBrands(APITestCase):
|
||||
"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())
|
||||
|
||||
@@ -5,6 +5,8 @@ from typing import Any
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import Value as V
|
||||
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.brands.models import Brand
|
||||
@@ -32,8 +34,13 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
|
||||
"""Context Processor that injects brand object into every template"""
|
||||
brand = getattr(request, "brand", DEFAULT_BRAND)
|
||||
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 {
|
||||
"brand": brand,
|
||||
"brand_css": brand_css,
|
||||
"footer_links": tenant.footer_links,
|
||||
"html_meta": {**get_http_meta()},
|
||||
"version": get_full_version(),
|
||||
|
||||
@@ -5,6 +5,7 @@ from contextvars import ContextVar
|
||||
from functools import partial
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth import logout
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
@@ -58,6 +59,11 @@ class AuthenticationMiddleware(MiddlewareMixin):
|
||||
request.user = SimpleLazyObject(lambda: get_user(request))
|
||||
request.auser = partial(aget_user, request)
|
||||
|
||||
user = request.user
|
||||
if user and user.is_authenticated and not user.is_active:
|
||||
logout(request)
|
||||
raise AssertionError()
|
||||
|
||||
|
||||
class ImpersonateMiddleware:
|
||||
"""Middleware to impersonate users"""
|
||||
|
||||
24
authentik/core/migrations/0049_alter_token_options.py
Normal file
24
authentik/core/migrations/0049_alter_token_options.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-03 13:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0048_delete_oldauthenticatedsession_content_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="token",
|
||||
options={
|
||||
"permissions": [
|
||||
("view_token_key", "View token's key"),
|
||||
("set_token_key", "Set a token's key"),
|
||||
],
|
||||
"verbose_name": "Token",
|
||||
"verbose_name_plural": "Tokens",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -953,7 +953,10 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
|
||||
models.Index(fields=["identifier"]),
|
||||
models.Index(fields=["key"]),
|
||||
]
|
||||
permissions = [("view_token_key", _("View token's key"))]
|
||||
permissions = [
|
||||
("view_token_key", _("View token's key")),
|
||||
("set_token_key", _("Set a token's key")),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
description = f"{self.identifier}"
|
||||
@@ -1082,6 +1085,12 @@ class AuthenticatedSession(SerializerModel):
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionSerializer
|
||||
|
||||
return AuthenticatedSessionSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Authenticated Session")
|
||||
verbose_name_plural = _("Authenticated Sessions")
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<style>{{ brand.branding_custom_css }}</style>
|
||||
<style>{{ brand_css }}</style>
|
||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
|
||||
@@ -16,7 +16,7 @@ from authentik.stages.authenticator.models import Device
|
||||
|
||||
|
||||
class AuthenticatorEndpointGDTCStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Setup Google Chrome Device-trust connection"""
|
||||
"""Setup Google Chrome Device Trust connection"""
|
||||
|
||||
credentials = models.JSONField()
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor):
|
||||
self.reader: Reader | None = None
|
||||
self._last_mtime: float = 0.0
|
||||
self.logger = get_logger()
|
||||
self.open()
|
||||
self.load()
|
||||
|
||||
def path(self) -> str | None:
|
||||
"""Get the path to the MMDB file to load"""
|
||||
raise NotImplementedError
|
||||
|
||||
def open(self):
|
||||
def load(self):
|
||||
"""Get GeoIP Reader, if configured, otherwise none"""
|
||||
path = self.path()
|
||||
if path == "" or not path:
|
||||
@@ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor):
|
||||
diff = self._last_mtime < mtime
|
||||
if diff > 0:
|
||||
self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
|
||||
self.open()
|
||||
self.load()
|
||||
except OSError as exc:
|
||||
self.logger.warning("Failed to check MMDB age", exc=exc)
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class SyncTasks:
|
||||
def sync_objects(
|
||||
self, object_type: str, page: int, provider_pk: int, override_dry_run=False, **filter
|
||||
):
|
||||
_object_type = path_to_class(object_type)
|
||||
_object_type: type[Model] = path_to_class(object_type)
|
||||
self.logger = get_logger().bind(
|
||||
provider_type=class_to_path(self._provider_model),
|
||||
provider_pk=provider_pk,
|
||||
@@ -156,7 +156,11 @@ class SyncTasks:
|
||||
messages.append(
|
||||
asdict(
|
||||
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",
|
||||
logger=f"{provider._meta.verbose_name}@{object_type}",
|
||||
)
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
"""Websocket tests"""
|
||||
|
||||
from dataclasses import asdict
|
||||
from unittest.mock import patch
|
||||
|
||||
from channels.routing import URLRouter
|
||||
from channels.testing import WebsocketCommunicator
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik import __version__
|
||||
@@ -16,12 +14,6 @@ from authentik.providers.proxy.models import ProxyProvider
|
||||
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):
|
||||
"""Websocket tests"""
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
# Buffer sizes for large headers with JWTs
|
||||
"nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
|
||||
"nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
|
||||
"nginx.ingress.kubernetes.io/proxy-busy-buffers-size": "32k",
|
||||
# Enable TLS in traefik
|
||||
"traefik.ingress.kubernetes.io/router.tls": "true",
|
||||
}
|
||||
|
||||
@@ -66,7 +66,10 @@ class RACClientConsumer(AsyncWebsocketConsumer):
|
||||
def init_outpost_connection(self):
|
||||
"""Initialize guac connection settings"""
|
||||
self.token = (
|
||||
ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"])
|
||||
ConnectionToken.filter_not_expired(
|
||||
token=self.scope["url_route"]["kwargs"]["token"],
|
||||
session__session__session_key=self.scope["session"].session_key,
|
||||
)
|
||||
.select_related("endpoint", "provider", "session", "session__user")
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -166,7 +166,6 @@ class ConnectionToken(ExpiringModel):
|
||||
always_merger.merge(settings, default_settings)
|
||||
always_merger.merge(settings, self.endpoint.provider.settings)
|
||||
always_merger.merge(settings, self.endpoint.settings)
|
||||
always_merger.merge(settings, self.settings)
|
||||
|
||||
def mapping_evaluator(mappings: QuerySet):
|
||||
for mapping in mappings:
|
||||
@@ -191,6 +190,7 @@ class ConnectionToken(ExpiringModel):
|
||||
mapping_evaluator(
|
||||
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["create-drive-path"] = "true"
|
||||
|
||||
@@ -90,23 +90,6 @@ class TestModels(TransactionTestCase):
|
||||
"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)
|
||||
mapping = RACPropertyMapping.objects.create(
|
||||
name=generate_id(),
|
||||
@@ -151,3 +134,22 @@ class TestModels(TransactionTestCase):
|
||||
"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",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -87,3 +87,22 @@ class TestRACViews(APITestCase):
|
||||
)
|
||||
body = loads(flow_response.content)
|
||||
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"))
|
||||
|
||||
@@ -20,6 +20,9 @@ from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
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):
|
||||
@@ -65,7 +68,10 @@ class RACInterface(InterfaceView):
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
# Early sanity check to ensure token still exists
|
||||
token = ConnectionToken.filter_not_expired(token=self.kwargs["token"]).first()
|
||||
token = ConnectionToken.filter_not_expired(
|
||||
token=self.kwargs["token"],
|
||||
session__session__session_key=request.session.session_key,
|
||||
).first()
|
||||
if not token:
|
||||
return redirect("authentik_core:if-user")
|
||||
self.token = token
|
||||
@@ -109,10 +115,15 @@ class RACFinalStage(RedirectStage):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
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(
|
||||
provider=self.provider,
|
||||
endpoint=self.endpoint,
|
||||
settings=self.executor.plan.context.get("connection_settings", {}),
|
||||
settings=settings or {},
|
||||
session=self.request.session["authenticatedsession"],
|
||||
expires=now() + timedelta_from_string(self.provider.connection_expiry),
|
||||
expiring=True,
|
||||
|
||||
@@ -3,25 +3,46 @@
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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.sentry import sentry_init
|
||||
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
|
||||
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
|
||||
"""Runs pytest to discover and run tests."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.logger = get_logger().bind(runner="pytest")
|
||||
|
||||
self.args = []
|
||||
if self.failfast:
|
||||
@@ -48,6 +69,10 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
|
||||
CONFIG.set("error_reporting.sample_rate", 0)
|
||||
CONFIG.set("error_reporting.environment", "testing")
|
||||
CONFIG.set("error_reporting.send_pii", True)
|
||||
|
||||
ASN_CONTEXT_PROCESSOR.load()
|
||||
GEOIP_CONTEXT_PROCESSOR.load()
|
||||
|
||||
sentry_init()
|
||||
|
||||
pre_startup.send(sender=self, mode="test")
|
||||
@@ -113,4 +138,10 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
|
||||
f"path instead."
|
||||
)
|
||||
|
||||
return pytest.main(self.args)
|
||||
self.logger.info("Running tests", test_files=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
|
||||
|
||||
@@ -71,37 +71,31 @@ def ldap_sync_single(source_pk: str):
|
||||
return
|
||||
# Delete all sync tasks from the cache
|
||||
DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
|
||||
task = chain(
|
||||
# User and group sync can happen at once, they have no dependencies on each other
|
||||
group(
|
||||
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),
|
||||
),
|
||||
|
||||
# The order of these operations needs to be preserved as each depends on the previous one(s)
|
||||
# 1. User and group sync can happen simultaneously
|
||||
# 2. Membership sync needs to run afterwards
|
||||
# 3. Finally, user and group deletions can happen simultaneously
|
||||
user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator(
|
||||
source, GroupLDAPSynchronizer
|
||||
)
|
||||
task()
|
||||
membership_sync = ldap_sync_paginator(source, MembershipLDAPSynchronizer)
|
||||
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:
|
||||
|
||||
@@ -151,9 +151,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
||||
webauthn_user_verification=UserVerification.PREFERRED,
|
||||
)
|
||||
stage.webauthn_allowed_device_types.set(
|
||||
WebAuthnDeviceType.objects.filter(
|
||||
description="Android Authenticator with SafetyNet Attestation"
|
||||
)
|
||||
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series")
|
||||
)
|
||||
session = self.client.session
|
||||
plan = FlowPlan(flow_pk=flow.pk.hex)
|
||||
@@ -339,9 +337,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
||||
device_classes=[DeviceClasses.WEBAUTHN],
|
||||
)
|
||||
stage.webauthn_allowed_device_types.set(
|
||||
WebAuthnDeviceType.objects.filter(
|
||||
description="Android Authenticator with SafetyNet Attestation"
|
||||
)
|
||||
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series")
|
||||
)
|
||||
session = self.client.session
|
||||
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
@@ -141,9 +141,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
|
||||
"""Test registration with restricted devices (fail)"""
|
||||
webauthn_mds_import.delay(force=True).get()
|
||||
self.stage.device_type_restrictions.set(
|
||||
WebAuthnDeviceType.objects.filter(
|
||||
description="Android Authenticator with SafetyNet Attestation"
|
||||
)
|
||||
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series")
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
|
||||
@@ -100,9 +100,11 @@ def send_mail(
|
||||
# Because we use the Message-ID as UID for the task, manually assign it
|
||||
message_object.extra_headers["Message-ID"] = message_id
|
||||
|
||||
# Add the logo (we can't add it in the previous message since MIMEImage
|
||||
# can't be converted to json)
|
||||
message_object.attach(logo_data())
|
||||
# Add the logo if it is used in the email body (we can't add it in the
|
||||
# previous message since MIMEImage can't be converted to json)
|
||||
body = get_email_body(message_object)
|
||||
if "cid:logo" in body:
|
||||
message_object.attach(logo_data())
|
||||
|
||||
if (
|
||||
message_object.to
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<table width="100%" style="background-color: #FFFFFF; border-spacing: 0; margin-top: 15px;">
|
||||
<tr height="80">
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<img src="{% block logo_url %}cid:logo.png{% endblock %}" border="0=" alt="authentik logo" class="flexibleImage logo">
|
||||
<img src="{% block logo_url %}cid:logo{% endblock %}" border="0=" alt="authentik logo" class="flexibleImage logo">
|
||||
</td>
|
||||
</tr>
|
||||
{% block content %}
|
||||
|
||||
@@ -19,7 +19,8 @@ def logo_data() -> MIMEImage:
|
||||
path = Path("web/dist/assets/icons/icon_left_brand.png")
|
||||
with open(path, "rb") as _logo_file:
|
||||
logo = MIMEImage(_logo_file.read())
|
||||
logo.add_header("Content-ID", "logo.png")
|
||||
logo.add_header("Content-ID", "<logo>")
|
||||
logo.add_header("Content-Disposition", "inline", filename="logo.png")
|
||||
return logo
|
||||
|
||||
|
||||
|
||||
@@ -89,6 +89,29 @@ class TestPasswordStage(FlowTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
|
||||
def test_valid_password_inactive(self):
|
||||
"""Test with a valid pending user and valid password"""
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
# Form data
|
||||
{"password": self.user.username},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
response_errors={"password": [{"string": "Invalid password", "code": "invalid"}]},
|
||||
)
|
||||
|
||||
def test_invalid_password(self):
|
||||
"""Test with a valid pending user and invalid password"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
|
||||
@@ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
|
||||
)
|
||||
if configured_binding_net != NetworkBinding.NO_BINDING:
|
||||
self.recheck_session_net(configured_binding_net, last_ip, new_ip)
|
||||
BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip)
|
||||
if configured_binding_geo != GeoIPBinding.NO_BINDING:
|
||||
self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
|
||||
BoundSessionMiddleware.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
|
||||
# If we got to this point without any error being raised, we need to
|
||||
# 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:
|
||||
@@ -111,7 +111,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
# (== basically requires the user to be logged in)
|
||||
request.session[request.session.model.Keys.LAST_IP] = new_ip
|
||||
|
||||
def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
|
||||
@staticmethod
|
||||
def recheck_session_net(binding: NetworkBinding, last_ip: str, new_ip: str):
|
||||
"""Check network/ASN binding"""
|
||||
last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
|
||||
new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
|
||||
@@ -158,7 +159,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
new_ip,
|
||||
)
|
||||
|
||||
def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
|
||||
@staticmethod
|
||||
def recheck_session_geo(binding: GeoIPBinding, last_ip: str, new_ip: str):
|
||||
"""Check GeoIP binding"""
|
||||
last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
|
||||
new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
|
||||
@@ -179,8 +181,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
if last_geo.continent != new_geo.continent:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.continent",
|
||||
last_geo.continent,
|
||||
new_geo.continent,
|
||||
last_geo.continent.to_dict(),
|
||||
new_geo.continent.to_dict(),
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
@@ -192,8 +194,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
if last_geo.country != new_geo.country:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.country",
|
||||
last_geo.country,
|
||||
new_geo.country,
|
||||
last_geo.country.to_dict(),
|
||||
new_geo.country.to_dict(),
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
@@ -202,8 +204,8 @@ class BoundSessionMiddleware(SessionMiddleware):
|
||||
if last_geo.city != new_geo.city:
|
||||
raise SessionBindingBroken(
|
||||
"geoip.city",
|
||||
last_geo.city,
|
||||
new_geo.city,
|
||||
last_geo.city.to_dict(),
|
||||
new_geo.city.to_dict(),
|
||||
last_ip,
|
||||
new_ip,
|
||||
)
|
||||
|
||||
@@ -91,6 +91,7 @@ class UserLoginStageView(ChallengeStageView):
|
||||
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
if not user.is_active:
|
||||
self.logger.warning("User is not active, login will not work.")
|
||||
return self.executor.stage_invalid()
|
||||
delta = self.set_session_duration(remember)
|
||||
self.set_session_ip()
|
||||
# the `user_logged_in` signal will update the user to write the `last_login` field
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
from time import sleep
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import AuthenticatedSession, Session
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.flows.markers import StageMarker
|
||||
@@ -17,7 +19,12 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.root.middleware import ClientIPMiddleware
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
from authentik.stages.user_login.middleware import (
|
||||
BoundSessionMiddleware,
|
||||
SessionBindingBroken,
|
||||
logout_extra,
|
||||
)
|
||||
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage
|
||||
|
||||
|
||||
class TestUserLoginStage(FlowTestCase):
|
||||
@@ -174,6 +181,7 @@ class TestUserLoginStage(FlowTestCase):
|
||||
component="ak-stage-access-denied",
|
||||
)
|
||||
|
||||
@apply_blueprint("default/flow-default-user-settings-flow.yaml")
|
||||
def test_inactive_account(self):
|
||||
"""Test with a valid pending user and backend"""
|
||||
self.user.is_active = False
|
||||
@@ -187,8 +195,74 @@ class TestUserLoginStage(FlowTestCase):
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
self.assertStageResponse(
|
||||
response, self.flow, component="ak-stage-access-denied", error_message="Unknown error"
|
||||
)
|
||||
|
||||
# Check that API requests get rejected
|
||||
response = self.client.get(reverse("authentik_api:application-list"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Check that flow requests requiring a user also get rejected
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:flow-executor",
|
||||
kwargs={"flow_slug": "default-user-settings-flow"},
|
||||
)
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
component="ak-stage-access-denied",
|
||||
error_message="Flow does not apply to current user.",
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.4.1 Blueprint schema",
|
||||
"title": "authentik 2025.6.4 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.4}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@@ -55,7 +55,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.4}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
||||
2
go.mod
2
go.mod
@@ -27,7 +27,7 @@ require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025041.2
|
||||
goauthentik.io/api/v3 v3.2025041.4
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.14.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025041.2 h1:vFYYnhcDcxL95RczZwhzt3i4LptFXMvIRN+vgf8sQYg=
|
||||
goauthentik.io/api/v3 v3.2025041.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025041.4 h1:cGqzWYnUHrWDoaXWDpIL/kWnX9sFrIhkYDye0P0OEAo=
|
||||
goauthentik.io/api/v3 v3.2025041.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
||||
@@ -33,4 +33,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.4.1"
|
||||
const VERSION = "2025.6.4"
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
@@ -118,8 +119,8 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
|
||||
mux := mux.NewRouter()
|
||||
|
||||
// Save cookie name, based on hashed client ID
|
||||
h := sha256.New()
|
||||
bs := string(h.Sum([]byte(*p.ClientId)))
|
||||
hs := sha256.Sum256([]byte(*p.ClientId))
|
||||
bs := hex.EncodeToString(hs[:])
|
||||
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
|
||||
|
||||
@@ -3,6 +3,7 @@ package application
|
||||
type ProxyClaims struct {
|
||||
UserAttributes map[string]interface{} `json:"user_attributes"`
|
||||
BackendOverride string `json:"backend_override"`
|
||||
HostHeader string `json:"host_header"`
|
||||
IsSuperuser bool `json:"is_superuser"`
|
||||
}
|
||||
|
||||
|
||||
@@ -74,13 +74,18 @@ func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
|
||||
r.URL.Scheme = ou.Scheme
|
||||
r.URL.Host = ou.Host
|
||||
claims := a.getClaimsFromSession(r)
|
||||
if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" {
|
||||
u, err := url.Parse(claims.Proxy.BackendOverride)
|
||||
if err != nil {
|
||||
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
|
||||
} else {
|
||||
r.URL.Scheme = u.Scheme
|
||||
r.URL.Host = u.Host
|
||||
if claims != nil && claims.Proxy != nil {
|
||||
if claims.Proxy.BackendOverride != "" {
|
||||
u, err := url.Parse(claims.Proxy.BackendOverride)
|
||||
if err != nil {
|
||||
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
|
||||
} else {
|
||||
r.URL.Scheme = u.Scheme
|
||||
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")
|
||||
|
||||
@@ -2,6 +2,7 @@ package radius
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -11,9 +12,7 @@ import (
|
||||
"layeh.com/radius/rfc2865"
|
||||
)
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
username := rfc2865.UserName_GetString(r.Packet)
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest_PAP_Auth(r *RadiusRequest, username, password string) (*radius.Packet, error) {
|
||||
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
"username": username,
|
||||
"client": r.RemoteAddr(),
|
||||
@@ -23,67 +22,64 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
|
||||
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||
|
||||
fe.Answers[flow.StageIdentification] = username
|
||||
fe.SetSecrets(rfc2865.UserPassword_GetString(r.Packet), r.pi.MFASupport)
|
||||
fe.SetSecrets(password, r.pi.MFASupport)
|
||||
|
||||
passed, err := fe.Execute()
|
||||
if err != nil {
|
||||
r.Log().WithField("username", username).WithError(err).Warning("failed to execute flow")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": rs.ac.Outpost.Name,
|
||||
"reason": "flow_error",
|
||||
"app": r.pi.appSlug,
|
||||
}).Inc()
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
return nil, errors.New("flow_error")
|
||||
}
|
||||
if !passed {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": rs.ac.Outpost.Name,
|
||||
"reason": "invalid_credentials",
|
||||
"app": r.pi.appSlug,
|
||||
}).Inc()
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
return nil, errors.New("invalid_credentials")
|
||||
}
|
||||
access, _, err := fe.ApiClient().OutpostsApi.OutpostsRadiusAccessCheck(
|
||||
r.Context(), r.pi.providerId,
|
||||
).AppSlug(r.pi.appSlug).Execute()
|
||||
if err != nil {
|
||||
r.Log().WithField("username", username).WithError(err).Warning("failed to check access")
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": rs.ac.Outpost.Name,
|
||||
"reason": "access_check_fail",
|
||||
"app": r.pi.appSlug,
|
||||
}).Inc()
|
||||
return
|
||||
return nil, errors.New("access_check_fail")
|
||||
}
|
||||
if !access.Access.Passing {
|
||||
r.Log().WithField("username", username).Info("Access denied for user")
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": rs.ac.Outpost.Name,
|
||||
"reason": "access_denied",
|
||||
"app": r.pi.appSlug,
|
||||
}).Inc()
|
||||
return
|
||||
return nil, errors.New("access_denied")
|
||||
}
|
||||
res := r.Response(radius.CodeAccessAccept)
|
||||
defer func() { _ = w.Write(res) }()
|
||||
if !access.HasAttributes() {
|
||||
r.Log().Debug("No attributes")
|
||||
return
|
||||
return res, nil
|
||||
}
|
||||
rawData, err := base64.StdEncoding.DecodeString(access.GetAttributes())
|
||||
if err != nil {
|
||||
r.Log().WithError(err).Warning("failed to decode attributes from core")
|
||||
return
|
||||
return nil, errors.New("attribute_decode_failed")
|
||||
}
|
||||
p, err := radius.Parse(rawData, r.pi.SharedSecret)
|
||||
if err != nil {
|
||||
r.Log().WithError(err).Warning("failed to parse attributes from core")
|
||||
return nil, errors.New("attribute_parse_failed")
|
||||
}
|
||||
for _, attr := range p.Attributes {
|
||||
res.Add(attr.Type, attr.Attribute)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
username := rfc2865.UserName_GetString(r.Packet)
|
||||
password := rfc2865.UserPassword_GetString(r.Packet)
|
||||
res, err := rs.Handle_AccessRequest_PAP_Auth(r, username, password)
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": rs.ac.Outpost.Name,
|
||||
"reason": err.Error(),
|
||||
"app": r.pi.appSlug,
|
||||
}).Inc()
|
||||
_ = w.Write(r.Reject())
|
||||
return
|
||||
}
|
||||
err = r.setMessageAuthenticator(res)
|
||||
if err != nil {
|
||||
rs.log.WithError(err).Warning("failed to set message authenticator")
|
||||
}
|
||||
_ = w.Write(res)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package radius
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
@@ -12,6 +17,11 @@ import (
|
||||
"goauthentik.io/internal/outpost/radius/metrics"
|
||||
"goauthentik.io/internal/utils"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/rfc2869"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidMessageAuthenticator = errors.New("invalid message authenticator")
|
||||
)
|
||||
|
||||
type RadiusRequest struct {
|
||||
@@ -34,6 +44,41 @@ func (r *RadiusRequest) ID() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
func (r *RadiusRequest) validateMessageAuthenticator() error {
|
||||
mauth := rfc2869.MessageAuthenticator_Get(r.Packet)
|
||||
hash := hmac.New(md5.New, r.Secret)
|
||||
encode, err := r.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash.Write(encode)
|
||||
if bytes.Equal(mauth, hash.Sum(nil)) {
|
||||
return ErrInvalidMessageAuthenticator
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RadiusRequest) setMessageAuthenticator(rp *radius.Packet) error {
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16))
|
||||
hash := hmac.New(md5.New, rp.Secret)
|
||||
encode, err := rp.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash.Write(encode)
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RadiusRequest) Reject() *radius.Packet {
|
||||
res := r.Response(radius.CodeAccessReject)
|
||||
err := r.setMessageAuthenticator(res)
|
||||
if err != nil {
|
||||
r.log.WithError(err).Warning("failed to set message authenticator")
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
|
||||
span := sentry.StartSpan(r.Context(), "authentik.providers.radius.connect",
|
||||
sentry.WithTransactionName("authentik.providers.radius.connect"))
|
||||
@@ -58,6 +103,11 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
||||
|
||||
rl.Info("Radius Request")
|
||||
|
||||
if err := nr.validateMessageAuthenticator(); err != nil {
|
||||
rl.WithError(err).Warning("Invalid message authenticator")
|
||||
return
|
||||
}
|
||||
|
||||
// Lookup provider by shared secret
|
||||
var pi *ProviderInstance
|
||||
for _, p := range rs.providers {
|
||||
@@ -68,8 +118,10 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
||||
}
|
||||
}
|
||||
if pi == nil {
|
||||
nr.Log().WithField("hashed_secret", string(sha512.New().Sum(r.Secret))).Warning("No provider found")
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
hs := sha512.Sum512([]byte(r.Secret))
|
||||
bs := hex.EncodeToString(hs[:])
|
||||
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
|
||||
_ = w.Write(nr.Reject())
|
||||
return
|
||||
}
|
||||
nr.pi = pi
|
||||
|
||||
@@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.4.1
|
||||
Default: 2025.6.4
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
|
||||
"POT-Creation-Date: 2025-06-02 00:12+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -2226,6 +2226,10 @@ msgstr ""
|
||||
msgid "Consider Objects matching this filter to be Users."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Attribute which matches the value of `group_membership_field`."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Field which contains members of a group."
|
||||
msgstr ""
|
||||
@@ -3493,10 +3497,6 @@ msgstr ""
|
||||
msgid "No Pending user to login."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/stage.py
|
||||
msgid "Successfully logged in!"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_logout/models.py
|
||||
msgid "User Logout Stage"
|
||||
msgstr ""
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.6.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.6.4",
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"prettier": "^3.3.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.6.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2025.4.1"
|
||||
version = "2025.6.4"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.13.*"
|
||||
@@ -13,7 +13,7 @@ dependencies = [
|
||||
"dacite==1.9.2",
|
||||
"deepmerge==2.0",
|
||||
"defusedxml==0.7.1",
|
||||
"django==5.1.9",
|
||||
"django==5.1.11",
|
||||
"django-countries==7.6.1",
|
||||
"django-cte==1.3.3",
|
||||
"django-filter==25.1",
|
||||
@@ -61,7 +61,7 @@ dependencies = [
|
||||
"setproctitle==1.3.6",
|
||||
"structlog==25.3.0",
|
||||
"swagger-spec-validator==3.0.4",
|
||||
"tenant-schemas-celery==4.0.1",
|
||||
"tenant-schemas-celery==3.0.0",
|
||||
"twilio==9.6.1",
|
||||
"ua-parser==1.0.1",
|
||||
"unidecode==1.4.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2025.4.1
|
||||
version: 2025.6.4
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
@@ -2,7 +2,6 @@
|
||||
|
||||
from dataclasses import asdict
|
||||
from time import sleep
|
||||
from unittest.mock import patch
|
||||
|
||||
from guardian.shortcuts import assign_perm
|
||||
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
||||
@@ -16,12 +15,10 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
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 tests.e2e.utils import SeleniumTestCase, retry
|
||||
|
||||
|
||||
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
|
||||
class TestProviderLDAP(SeleniumTestCase):
|
||||
"""LDAP and Outpost e2e tests"""
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from json import loads
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from unittest.case import skip, skipUnless
|
||||
from unittest.mock import patch
|
||||
|
||||
from channels.testing import ChannelsLiveServerTestCase
|
||||
from jwt import decode
|
||||
@@ -18,12 +17,10 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
|
||||
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 tests.e2e.utils import SeleniumTestCase, retry
|
||||
|
||||
|
||||
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
|
||||
class TestProviderProxy(SeleniumTestCase):
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from json import loads
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
from unittest import skip
|
||||
from unittest.mock import patch
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
@@ -13,12 +12,10 @@ from authentik.core.models import Application
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
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 tests.e2e.utils import SeleniumTestCase, retry
|
||||
|
||||
|
||||
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
|
||||
class TestProviderProxyForward(SeleniumTestCase):
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
from dataclasses import asdict
|
||||
from time import sleep
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyrad.client import Client
|
||||
from pyrad.client import Client, Timeout
|
||||
from pyrad.dictionary import Dictionary
|
||||
from pyrad.packet import AccessAccept, AccessReject, AccessRequest
|
||||
|
||||
@@ -13,12 +12,10 @@ from authentik.core.models import Application, User
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
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 tests.e2e.utils import SeleniumTestCase, retry
|
||||
|
||||
|
||||
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
|
||||
class TestProviderRadius(SeleniumTestCase):
|
||||
"""Radius Outpost e2e tests"""
|
||||
|
||||
@@ -30,7 +27,7 @@ class TestProviderRadius(SeleniumTestCase):
|
||||
"""Start radius container based on outpost created"""
|
||||
self.run_container(
|
||||
image=self.get_container_image("ghcr.io/goauthentik/dev-radius"),
|
||||
ports={"1812/udp": "1812/udp"},
|
||||
ports={"1812/udp": 1812},
|
||||
environment={
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
@@ -66,7 +63,7 @@ class TestProviderRadius(SeleniumTestCase):
|
||||
sleep(5)
|
||||
return outpost
|
||||
|
||||
@retry()
|
||||
@retry(exceptions=[Timeout])
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
@@ -88,7 +85,7 @@ class TestProviderRadius(SeleniumTestCase):
|
||||
reply = srv.SendPacket(req)
|
||||
self.assertEqual(reply.code, AccessAccept)
|
||||
|
||||
@retry()
|
||||
@retry(exceptions=[Timeout])
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""authentik e2e testing utilities"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache, wraps
|
||||
@@ -37,22 +36,12 @@ from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.root.test_runner import get_docker_tag
|
||||
|
||||
IS_CI = "CI" in environ
|
||||
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:
|
||||
"""Get the local machine's IP"""
|
||||
hostname = socket.gethostname()
|
||||
|
||||
71
uv.lock
generated
71
uv.lock
generated
@@ -164,7 +164,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authentik"
|
||||
version = "2025.4.1"
|
||||
version = "2025.6.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "argon2-cffi" },
|
||||
@@ -273,7 +273,7 @@ requires-dist = [
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
{ name = "deepmerge", specifier = "==2.0" },
|
||||
{ name = "defusedxml", specifier = "==0.7.1" },
|
||||
{ name = "django", specifier = "==5.1.9" },
|
||||
{ name = "django", specifier = "==5.1.11" },
|
||||
{ name = "django-countries", specifier = "==7.6.1" },
|
||||
{ name = "django-cte", specifier = "==1.3.3" },
|
||||
{ name = "django-filter", specifier = "==25.1" },
|
||||
@@ -321,7 +321,7 @@ requires-dist = [
|
||||
{ name = "setproctitle", specifier = "==1.3.6" },
|
||||
{ name = "structlog", specifier = "==25.3.0" },
|
||||
{ name = "swagger-spec-validator", specifier = "==3.0.4" },
|
||||
{ name = "tenant-schemas-celery", specifier = "==4.0.1" },
|
||||
{ name = "tenant-schemas-celery", specifier = "==3.0.0" },
|
||||
{ name = "twilio", specifier = "==9.6.1" },
|
||||
{ name = "ua-parser", specifier = "==1.0.1" },
|
||||
{ name = "unidecode", specifier = "==1.4.0" },
|
||||
@@ -979,16 +979,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.1.9"
|
||||
version = "5.1.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2383,16 +2383,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.30.2"
|
||||
version = "6.31.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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/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/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/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/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/e5/a1/93c2acf4ade3c5b557d02d500b06798f4ed2c176fa03e3c34973ca92df7f/protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51", size = 167062, upload-time = "2025-03-26T19:12:55.892Z" },
|
||||
{ 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/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/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/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/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/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2776,7 +2776,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
version = "2.32.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
@@ -2784,9 +2784,9 @@ dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3100,32 +3100,33 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tenant-schemas-celery"
|
||||
version = "4.0.1"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "celery" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.4.2"
|
||||
version = "6.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
|
||||
@@ -3287,11 +3288,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
@@ -85,8 +85,8 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
render(): TemplateResult {
|
||||
const username = this.user?.user.name || this.user?.user.username;
|
||||
|
||||
return html` <ak-page-header
|
||||
header=${msg(str`Welcome, ${username || ""}.`)}
|
||||
return html`<ak-page-header
|
||||
header=${this.user ? msg(str`Welcome, ${username || ""}.`) : msg("Welcome.")}
|
||||
description=${msg("General system status")}
|
||||
?hasIcon=${false}
|
||||
>
|
||||
|
||||
@@ -361,7 +361,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
||||
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Addition User DN")}
|
||||
label=${msg("Additional User DN")}
|
||||
name="additionalUserDn"
|
||||
>
|
||||
<input
|
||||
@@ -374,7 +374,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Addition Group DN")}
|
||||
label=${msg("Additional Group DN")}
|
||||
name="additionalGroupDn"
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -73,6 +73,12 @@ html > form > input {
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Form */
|
||||
|
||||
.pf-c-form {
|
||||
--pf-c-form__group--m-action--MarginTop: var(--pf-global--spacer--form-element);
|
||||
}
|
||||
|
||||
/* #region Icons */
|
||||
|
||||
.pf-icon {
|
||||
|
||||
@@ -201,6 +201,10 @@ select[multiple] option:checked {
|
||||
--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--BorderTopColor: transparent !important;
|
||||
--pf-c-form-control--BorderRightColor: transparent !important;
|
||||
|
||||
@@ -374,7 +374,7 @@ ${JSON.stringify(value.new_value, null, 4)}</pre
|
||||
|
||||
renderEmailSent() {
|
||||
let body = this.event.context.body as string;
|
||||
body = body.replace("cid:logo.png", "/static/dist/assets/icons/icon_left_brand.png");
|
||||
body = body.replace("cid:logo", "/static/dist/assets/icons/icon_left_brand.png");
|
||||
return html`<div class="pf-c-card__title">${msg("Email info:")}</div>
|
||||
<div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div>
|
||||
<ak-expand>
|
||||
|
||||
@@ -147,7 +147,7 @@ export class AKPageNavbar
|
||||
}
|
||||
|
||||
.accent-icon {
|
||||
height: 1em;
|
||||
height: 1.2em;
|
||||
width: 1em;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -157,6 +157,7 @@ export class AKPageNavbar
|
||||
}
|
||||
|
||||
&.page-description {
|
||||
padding-top: 0.3em;
|
||||
grid-area: description;
|
||||
margin-block-end: var(--pf-global--spacer--md);
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@ export class AKElement extends LitElement {
|
||||
applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet);
|
||||
|
||||
this.activeTheme = UiThemeEnum.Dark;
|
||||
} else if (this.preferredColorScheme === "light") {
|
||||
applyUITheme(nextStyleRoot, UiThemeEnum.Light, this.#customCSSStyleSheet);
|
||||
this.activeTheme = UiThemeEnum.Light;
|
||||
} else if (this.preferredColorScheme === "auto") {
|
||||
createUIThemeEffect(
|
||||
(nextUITheme) => {
|
||||
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
} from "./types.js";
|
||||
|
||||
function localeComparator(a: DualSelectPair, b: DualSelectPair) {
|
||||
const aSortBy = a[2];
|
||||
const bSortBy = b[2];
|
||||
const aSortBy = String(a[2] || a[0]);
|
||||
const bSortBy = String(b[2] || b[0]);
|
||||
|
||||
return aSortBy.localeCompare(bSortBy);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export type DualSelectPair<T = unknown> = [
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -17,7 +17,7 @@ import "@goauthentik/elements/table/TableSearch";
|
||||
import { SlottedTemplateResult } from "@goauthentik/elements/types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@@ -107,6 +107,9 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
|
||||
private isLoading = false;
|
||||
|
||||
#pageParam = `${this.tagName.toLowerCase()}-page`;
|
||||
#searchParam = `${this.tagName.toLowerCase()}-search`;
|
||||
|
||||
searchEnabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
@@ -123,7 +126,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
data?: PaginatedResponse<T>;
|
||||
|
||||
@property({ type: Number })
|
||||
page = getURLParam("tablePage", 1);
|
||||
page = getURLParam(this.#pageParam, 1);
|
||||
|
||||
/**
|
||||
* Set if your `selectedElements` use of the selection box is to enable bulk-delete,
|
||||
@@ -209,7 +212,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
await this.fetch();
|
||||
});
|
||||
if (this.searchEnabled()) {
|
||||
this.search = getURLParam("search", "");
|
||||
this.search = getURLParam(this.#searchParam, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,12 +465,23 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("page")) {
|
||||
updateURLParams({
|
||||
[this.#pageParam]: changedProperties.get("page"),
|
||||
});
|
||||
}
|
||||
if (changedProperties.has("search")) {
|
||||
updateURLParams({
|
||||
[this.#searchParam]: changedProperties.get("search"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderSearch(): TemplateResult {
|
||||
const runSearch = (value: string) => {
|
||||
this.search = value;
|
||||
updateURLParams({
|
||||
search: value,
|
||||
});
|
||||
this.page = 1;
|
||||
this.fetch();
|
||||
};
|
||||
|
||||
@@ -548,7 +562,6 @@ export abstract class Table<T> extends AKElement implements TableLike {
|
||||
/* A simple pagination display, shown at both the top and bottom of the page. */
|
||||
renderTablePagination(): TemplateResult {
|
||||
const handler = (page: number) => {
|
||||
updateURLParams({ tablePage: page });
|
||||
this.page = page;
|
||||
this.fetch();
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { updateURLParams } from "#elements/router/RouteMatch";
|
||||
import { Table } from "#elements/table/Table";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult } from "lit";
|
||||
import { CSSResult, nothing } from "lit";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@@ -44,7 +44,7 @@ export abstract class TablePage<T> extends Table<T> {
|
||||
? inner
|
||||
: html`<ak-empty-state icon=${this.pageIcon()} header="${msg("No objects found.")}">
|
||||
<div slot="body">
|
||||
${this.searchEnabled() ? this.renderEmptyClearSearch() : html``}
|
||||
${this.searchEnabled() ? this.renderEmptyClearSearch() : nothing}
|
||||
</div>
|
||||
<div slot="primary">${this.renderObjectCreate()}</div>
|
||||
</ak-empty-state>`}
|
||||
@@ -60,9 +60,7 @@ export abstract class TablePage<T> extends Table<T> {
|
||||
this.search = "";
|
||||
this.requestUpdate();
|
||||
this.fetch();
|
||||
updateURLParams({
|
||||
search: "",
|
||||
});
|
||||
this.page = 1;
|
||||
}}
|
||||
class="pf-c-button pf-m-link"
|
||||
>
|
||||
|
||||
@@ -1,55 +1,31 @@
|
||||
/**
|
||||
* @file IFrame Utilities
|
||||
*/
|
||||
import { renderStaticHTMLUnsafe } from "#common/purify";
|
||||
|
||||
interface IFrameLoadResult {
|
||||
contentWindow: Window;
|
||||
contentDocument: Document;
|
||||
}
|
||||
import { MaybeCompiledTemplateResult } from "lit";
|
||||
|
||||
export function pluckIFrameContent(iframe: HTMLIFrameElement) {
|
||||
const contentWindow = iframe.contentWindow;
|
||||
const contentDocument = iframe.contentDocument;
|
||||
|
||||
if (!contentWindow) {
|
||||
throw new Error("Iframe contentWindow is not accessible");
|
||||
}
|
||||
|
||||
if (!contentDocument) {
|
||||
throw new Error("Iframe contentDocument is not accessible");
|
||||
}
|
||||
|
||||
return {
|
||||
contentWindow,
|
||||
contentDocument,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveIFrameContent(iframe: HTMLIFrameElement): Promise<IFrameLoadResult> {
|
||||
if (iframe.contentDocument?.readyState === "complete") {
|
||||
return Promise.resolve(pluckIFrameContent(iframe));
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
iframe.addEventListener("load", () => resolve(pluckIFrameContent(iframe)), { once: true });
|
||||
});
|
||||
export interface CreateHTMLObjectInit {
|
||||
body: string | MaybeCompiledTemplateResult;
|
||||
head?: string | MaybeCompiledTemplateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal HTML wrapper for an iframe.
|
||||
* Render untrusted HTML to a string without escaping it.
|
||||
*
|
||||
* @deprecated Use the `contentDocument.body` directly instead.
|
||||
* @returns {string} The rendered HTML string.
|
||||
*/
|
||||
export function createIFrameHTMLWrapper(bodyContent: string): string {
|
||||
const html = String.raw;
|
||||
export function createDocumentTemplate(init: CreateHTMLObjectInit): string {
|
||||
const body = renderStaticHTMLUnsafe(init.body);
|
||||
const head = init.head ? renderStaticHTMLUnsafe(init.head) : "";
|
||||
|
||||
return html`<!doctype html>
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
${head}
|
||||
</head>
|
||||
<body style="display:flex;flex-direction:row;justify-content:center;">
|
||||
${bodyContent}
|
||||
<body>
|
||||
${body}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export class BaseDeviceStage<
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 0;
|
||||
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
|
||||
margin-bottom: var(--pf-c-form__group--m-action--MarginTop);
|
||||
flex-direction: column;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -55,7 +55,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 0;
|
||||
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
|
||||
margin-bottom: var(--pf-c-form__group--m-action--MarginTop);
|
||||
flex-direction: column;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -81,4 +81,22 @@ export const ChallengeTurnstileForce = captchaFactory({
|
||||
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
siteKey: "3x00000000000000000000FF",
|
||||
interactive: true,
|
||||
} as CaptchaChallenge);
|
||||
flowInfo: {
|
||||
layout: "stacked",
|
||||
cancelUrl: "",
|
||||
title: "Foo",
|
||||
},
|
||||
});
|
||||
|
||||
export const ChallengeRecaptcha = captchaFactory({
|
||||
pendingUser: "foo",
|
||||
pendingUserAvatar: "https://picsum.photos/64",
|
||||
jsUrl: "https://www.google.com/recaptcha/api.js",
|
||||
siteKey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
|
||||
interactive: true,
|
||||
flowInfo: {
|
||||
layout: "stacked",
|
||||
cancelUrl: "",
|
||||
title: "Foo",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
/// <reference types="@hcaptcha/types"/>
|
||||
/// <reference types="turnstile-types"/>
|
||||
import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import { akEmptyState } from "@goauthentik/elements/EmptyState";
|
||||
import { bound } from "@goauthentik/elements/decorators/bound";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe";
|
||||
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
|
||||
import { randomId } from "@goauthentik/elements/utils/randomId";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import { P, match } from "ts-pattern";
|
||||
import { pluckErrorDetail } from "#common/errors/network";
|
||||
import { akEmptyState } from "#elements/EmptyState";
|
||||
import "#elements/forms/FormElement";
|
||||
import { ListenerController } from "#elements/utils/listenerController";
|
||||
import { randomId } from "#elements/utils/randomId";
|
||||
import "#flow/FormStatic";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import { CaptchaHandler, iframeTemplate } from "#flow/stages/captcha/shared";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
import { CSSResult, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
@@ -25,209 +22,161 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
|
||||
|
||||
type TokenHandler = (token: string) => void;
|
||||
export type TokenListener = (token: string) => void;
|
||||
|
||||
type Dims = { height: number };
|
||||
|
||||
type IframeCaptchaMessage = {
|
||||
interface CaptchaMessage {
|
||||
source?: string;
|
||||
context?: string;
|
||||
message: "captcha";
|
||||
token: string;
|
||||
};
|
||||
}
|
||||
|
||||
type IframeResizeMessage = {
|
||||
interface LoadMessage {
|
||||
source?: string;
|
||||
context?: string;
|
||||
message: "resize";
|
||||
size: Dims;
|
||||
};
|
||||
|
||||
type IframeMessageEvent = MessageEvent<IframeCaptchaMessage | IframeResizeMessage>;
|
||||
|
||||
type CaptchaHandler = {
|
||||
name: string;
|
||||
interactive: () => Promise<unknown>;
|
||||
execute: () => Promise<unknown>;
|
||||
refreshInteractive: () => Promise<unknown>;
|
||||
refresh: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
// A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces
|
||||
// a resize. Because the Captcha is itself in an iframe, the reported height is often off by some
|
||||
// margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden
|
||||
// rendering.
|
||||
function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult {
|
||||
return html` ${children}
|
||||
<script>
|
||||
new ResizeObserver((entries) => {
|
||||
const height =
|
||||
document.body.offsetHeight +
|
||||
parseFloat(getComputedStyle(document.body).fontSize) * 2;
|
||||
|
||||
window.parent.postMessage({
|
||||
message: "resize",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor",
|
||||
size: { height },
|
||||
});
|
||||
}).observe(document.querySelector(".ak-captcha-container"));
|
||||
</script>
|
||||
|
||||
<script src=${challengeURL}></script>
|
||||
|
||||
<script>
|
||||
function callback(token) {
|
||||
window.parent.postMessage({
|
||||
message: "captcha",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor",
|
||||
token,
|
||||
});
|
||||
}
|
||||
</script>`;
|
||||
message: "load";
|
||||
}
|
||||
|
||||
type IframeMessageEvent = MessageEvent<CaptchaMessage | LoadMessage>;
|
||||
|
||||
@customElement("ak-stage-captcha")
|
||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
css`
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
css`
|
||||
:host {
|
||||
--captcha-background-to: var(--pf-global--BackgroundColor--light-100);
|
||||
--captcha-background-from: var(--pf-global--BackgroundColor--light-300);
|
||||
}
|
||||
|
||||
:host([theme="dark"]) {
|
||||
--captcha-background-to: var(--ak-dark-background-light);
|
||||
--captcha-background-from: var(--ak-dark-background-light-ish);
|
||||
}
|
||||
|
||||
@keyframes captcha-background-animation {
|
||||
0% {
|
||||
background-color: var(--captcha-background-from);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
50% {
|
||||
background-color: var(--captcha-background-to);
|
||||
}
|
||||
100% {
|
||||
background-color: var(--captcha-background-from);
|
||||
}
|
||||
}
|
||||
|
||||
#ak-captcha {
|
||||
width: 100%;
|
||||
min-height: 65px;
|
||||
|
||||
&[data-ready="loading"] {
|
||||
background-color: var(--captcha-background-from);
|
||||
animation: captcha-background-animation 1s infinite
|
||||
var(--pf-global--TimingFunction);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
//#region Properties
|
||||
|
||||
@property({ type: Boolean })
|
||||
embedded = false;
|
||||
public embedded = false;
|
||||
|
||||
@property()
|
||||
onTokenChange: TokenHandler = (token: string) => {
|
||||
public onTokenChange: TokenListener = (token: string) => {
|
||||
this.host.submit({ component: "ak-stage-captcha", token });
|
||||
};
|
||||
|
||||
@property()
|
||||
public onLoad?: () => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
refreshedAt = new Date();
|
||||
public refreshedAt = new Date();
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region State
|
||||
|
||||
@state()
|
||||
activeHandler?: CaptchaHandler = undefined;
|
||||
protected activeHandler: CaptchaHandler | null = null;
|
||||
|
||||
@state()
|
||||
error?: string;
|
||||
protected error: string | null = null;
|
||||
|
||||
handlers: CaptchaHandler[] = [
|
||||
{
|
||||
name: "grecaptcha",
|
||||
interactive: this.renderGReCaptchaFrame,
|
||||
execute: this.executeGReCaptcha,
|
||||
refreshInteractive: this.refreshGReCaptchaFrame,
|
||||
refresh: this.refreshGReCaptcha,
|
||||
},
|
||||
{
|
||||
name: "hcaptcha",
|
||||
interactive: this.renderHCaptchaFrame,
|
||||
execute: this.executeHCaptcha,
|
||||
refreshInteractive: this.refreshHCaptchaFrame,
|
||||
refresh: this.refreshHCaptcha,
|
||||
},
|
||||
{
|
||||
name: "turnstile",
|
||||
interactive: this.renderTurnstileFrame,
|
||||
execute: this.executeTurnstile,
|
||||
refreshInteractive: this.refreshTurnstileFrame,
|
||||
refresh: this.refreshTurnstile,
|
||||
},
|
||||
];
|
||||
@state()
|
||||
protected iframeHeight = 65;
|
||||
|
||||
_captchaFrame?: HTMLIFrameElement;
|
||||
_captchaDocumentContainer?: HTMLDivElement;
|
||||
_listenController = new ListenerController();
|
||||
#scriptElement?: HTMLScriptElement;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("message", this.onIframeMessage, {
|
||||
signal: this._listenController.signal,
|
||||
});
|
||||
}
|
||||
#iframeSource = "about:blank";
|
||||
#iframeRef = createRef<HTMLIFrameElement>();
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._listenController.abort();
|
||||
if (!this.challenge?.interactive) {
|
||||
if (document.body.contains(this.captchaDocumentContainer)) {
|
||||
document.body.removeChild(this.captchaDocumentContainer);
|
||||
}
|
||||
#iframeLoaded = false;
|
||||
|
||||
#captchaDocumentContainer?: HTMLDivElement;
|
||||
#listenController = new ListenerController();
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Getters/Setters
|
||||
|
||||
protected get captchaDocumentContainer(): HTMLDivElement {
|
||||
if (this.#captchaDocumentContainer) {
|
||||
return this.#captchaDocumentContainer;
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
|
||||
this.#captchaDocumentContainer = document.createElement("div");
|
||||
this.#captchaDocumentContainer.id = `ak-captcha-${randomId()}`;
|
||||
|
||||
return this.#captchaDocumentContainer;
|
||||
}
|
||||
|
||||
get captchaDocumentContainer(): HTMLDivElement {
|
||||
if (this._captchaDocumentContainer) {
|
||||
return this._captchaDocumentContainer;
|
||||
}
|
||||
this._captchaDocumentContainer = document.createElement("div");
|
||||
this._captchaDocumentContainer.id = `ak-captcha-${randomId()}`;
|
||||
return this._captchaDocumentContainer;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
get captchaFrame(): HTMLIFrameElement {
|
||||
if (this._captchaFrame) {
|
||||
return this._captchaFrame;
|
||||
}
|
||||
this._captchaFrame = document.createElement("iframe");
|
||||
this._captchaFrame.src = "about:blank";
|
||||
this._captchaFrame.id = `ak-captcha-${randomId()}`;
|
||||
return this._captchaFrame;
|
||||
}
|
||||
|
||||
onFrameResize({ height }: Dims) {
|
||||
this.captchaFrame.style.height = `${height}px`;
|
||||
}
|
||||
//#region Listeners
|
||||
|
||||
// ADR: Did not to put anything into `otherwise` or `exhaustive` here because iframe messages
|
||||
// that were not of interest to us also weren't necessarily corrupt or suspicious. For example,
|
||||
// during testing Storybook throws a lot of cross-iframe messages that we don't care about.
|
||||
|
||||
@bound
|
||||
onIframeMessage({ data }: IframeMessageEvent) {
|
||||
match(data)
|
||||
.with(
|
||||
{ source: "goauthentik.io", context: "flow-executor", message: "captcha" },
|
||||
({ token }) => this.onTokenChange(token),
|
||||
)
|
||||
.with(
|
||||
{ source: "goauthentik.io", context: "flow-executor", message: "resize" },
|
||||
({ size }) => this.onFrameResize(size),
|
||||
)
|
||||
.with(
|
||||
{ source: "goauthentik.io", context: "flow-executor", message: P.any },
|
||||
({ message }) => {
|
||||
console.debug(`authentik/stages/captcha: Unknown message: ${message}`);
|
||||
},
|
||||
)
|
||||
.otherwise(() => {});
|
||||
}
|
||||
#messageListener = ({ data }: IframeMessageEvent) => {
|
||||
if (!data) return;
|
||||
|
||||
async renderGReCaptchaFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="g-recaptcha ak-captcha-container"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`,
|
||||
);
|
||||
}
|
||||
if (data.source !== "goauthentik.io" || data.context !== "flow-executor") {
|
||||
return;
|
||||
}
|
||||
|
||||
return match(data)
|
||||
.with({ message: "captcha" }, ({ token }) => this.onTokenChange(token))
|
||||
.with({ message: "load" }, this.#loadListener)
|
||||
.otherwise(({ message }) => {
|
||||
console.debug(`authentik/stages/captcha: Unknown message: ${message}`);
|
||||
});
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region g-recaptcha
|
||||
|
||||
protected renderGReCaptchaFrame = () => {
|
||||
return html`<div
|
||||
id="ak-container"
|
||||
class="g-recaptcha"
|
||||
data-theme="${this.activeTheme}"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`;
|
||||
};
|
||||
|
||||
async executeGReCaptcha() {
|
||||
return grecaptcha.ready(() => {
|
||||
grecaptcha.execute(
|
||||
return grecaptcha.execute(
|
||||
grecaptcha.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
@@ -238,7 +187,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
}
|
||||
|
||||
async refreshGReCaptchaFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.grecaptcha.reset();
|
||||
this.#iframeRef.value?.contentWindow?.grecaptcha.reset();
|
||||
}
|
||||
|
||||
async refreshGReCaptcha() {
|
||||
@@ -246,19 +195,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
window.grecaptcha.execute();
|
||||
}
|
||||
|
||||
async renderHCaptchaFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="h-captcha ak-captcha-container"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-theme="${this.activeTheme ? this.activeTheme : "light"}"
|
||||
data-callback="callback"
|
||||
></div> `,
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region h-captcha
|
||||
|
||||
protected renderHCaptchaFrame = () => {
|
||||
return html`<div
|
||||
id="ak-container"
|
||||
class="h-captcha"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-theme="${this.activeTheme}"
|
||||
data-callback="callback"
|
||||
></div>`;
|
||||
};
|
||||
|
||||
async executeHCaptcha() {
|
||||
return hcaptcha.execute(
|
||||
await hcaptcha.execute(
|
||||
hcaptcha.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
@@ -268,7 +220,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
}
|
||||
|
||||
async refreshHCaptchaFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.hcaptcha.reset();
|
||||
this.#iframeRef.value?.contentWindow?.hcaptcha?.reset();
|
||||
}
|
||||
|
||||
async refreshHCaptcha() {
|
||||
@@ -276,61 +228,87 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
window.hcaptcha.execute();
|
||||
}
|
||||
|
||||
async renderTurnstileFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
class="cf-turnstile ak-captcha-container"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-callback="callback"
|
||||
></div>`,
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Turnstile
|
||||
|
||||
protected renderTurnstileFrame = () => {
|
||||
return html`<div
|
||||
id="ak-container"
|
||||
class="cf-turnstile"
|
||||
data-sitekey="${this.challenge.siteKey}"
|
||||
data-theme="${this.activeTheme}"
|
||||
data-callback="callback"
|
||||
data-size="flexible"
|
||||
></div>`;
|
||||
};
|
||||
|
||||
async executeTurnstile() {
|
||||
return window.turnstile.render(this.captchaDocumentContainer, {
|
||||
window.turnstile.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
});
|
||||
}
|
||||
|
||||
async refreshTurnstileFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.turnstile.reset();
|
||||
this.#iframeRef.value?.contentWindow?.turnstile.reset();
|
||||
}
|
||||
|
||||
async refreshTurnstile() {
|
||||
window.turnstile.reset();
|
||||
}
|
||||
|
||||
async renderFrame(captchaElement: TemplateResult) {
|
||||
const { contentDocument } = this.captchaFrame || {};
|
||||
//#endregion
|
||||
|
||||
if (!contentDocument) {
|
||||
console.debug(
|
||||
"authentik/stages/captcha: unable to render captcha frame, no contentDocument",
|
||||
);
|
||||
#handlers = new Map<string, CaptchaHandler>([
|
||||
[
|
||||
"grecaptcha",
|
||||
{
|
||||
interactive: this.renderGReCaptchaFrame,
|
||||
execute: this.executeGReCaptcha,
|
||||
refreshInteractive: this.refreshGReCaptchaFrame,
|
||||
refresh: this.refreshGReCaptcha,
|
||||
},
|
||||
],
|
||||
[
|
||||
"hcaptcha",
|
||||
{
|
||||
interactive: this.renderHCaptchaFrame,
|
||||
execute: this.executeHCaptcha,
|
||||
refreshInteractive: this.refreshHCaptchaFrame,
|
||||
refresh: this.refreshHCaptcha,
|
||||
},
|
||||
],
|
||||
[
|
||||
"turnstile",
|
||||
{
|
||||
interactive: this.renderTurnstileFrame,
|
||||
refreshInteractive: this.refreshTurnstileFrame,
|
||||
execute: this.executeTurnstile,
|
||||
refresh: this.refreshTurnstile,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
contentDocument.open();
|
||||
|
||||
contentDocument.write(
|
||||
createIFrameHTMLWrapper(
|
||||
renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)),
|
||||
),
|
||||
);
|
||||
|
||||
contentDocument.close();
|
||||
}
|
||||
//#region Render
|
||||
|
||||
renderBody() {
|
||||
// [hasError, isInteractive]
|
||||
// prettier-ignore
|
||||
return match([Boolean(this.error), Boolean(this.challenge?.interactive)])
|
||||
.with([true, P.any], () => akEmptyState({ icon: "fa-times", header: this.error }))
|
||||
.with([false, true], () => html`${this.captchaFrame}`)
|
||||
.with([false, false], () => akEmptyState({ loading: true, header: msg("Verifying...") }))
|
||||
.exhaustive();
|
||||
if (this.error) {
|
||||
return akEmptyState({ icon: "fa-times", header: this.error });
|
||||
}
|
||||
|
||||
if (this.challenge?.interactive) {
|
||||
return html`
|
||||
<iframe
|
||||
${ref(this.#iframeRef)}
|
||||
style="height: ${this.iframeHeight}px;"
|
||||
data-ready="${this.#iframeLoaded ? "ready" : "loading"}"
|
||||
id="ak-captcha"
|
||||
></iframe>
|
||||
`;
|
||||
}
|
||||
|
||||
return akEmptyState({ loading: true, header: msg("Verifying...") });
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
@@ -359,76 +337,165 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
}
|
||||
|
||||
render() {
|
||||
// [isEmbedded, hasChallenge, isInteractive]
|
||||
// prettier-ignore
|
||||
return match([this.embedded, Boolean(this.challenge), Boolean(this.challenge?.interactive)])
|
||||
.with([true, false, P.any], () => nothing)
|
||||
.with([true, true, false], () => nothing)
|
||||
.with([true, true, true], () => this.renderBody())
|
||||
.with([false, false, P.any], () => akEmptyState({ loading: true }))
|
||||
.with([false, true, P.any], () => this.renderMain())
|
||||
.exhaustive();
|
||||
if (!this.challenge) {
|
||||
return this.embedded ? nothing : akEmptyState({ loading: true });
|
||||
}
|
||||
|
||||
if (!this.embedded) {
|
||||
return this.renderMain();
|
||||
}
|
||||
|
||||
return this.challenge.interactive ? this.renderBody() : nothing;
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
if (!(changedProperties.has("challenge") && this.challenge !== undefined)) {
|
||||
//#endregion;
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("message", this.#messageListener, {
|
||||
signal: this.#listenController.signal,
|
||||
});
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
this.#listenController.abort();
|
||||
|
||||
if (!this.challenge?.interactive) {
|
||||
if (document.body.contains(this.captchaDocumentContainer)) {
|
||||
document.body.removeChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
public firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
if (!(changedProperties.has("challenge") && typeof this.challenge !== "undefined")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachCaptcha = async () => {
|
||||
console.debug("authentik/stages/captcha: script loaded");
|
||||
const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name));
|
||||
let lastError = undefined;
|
||||
let found = false;
|
||||
for (const handler of handlers) {
|
||||
console.debug(`authentik/stages/captcha: trying handler ${handler.name}`);
|
||||
try {
|
||||
const runner = this.challenge.interactive
|
||||
? handler.interactive
|
||||
: handler.execute;
|
||||
await runner.apply(this);
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: handler succeeded`);
|
||||
found = true;
|
||||
this.activeHandler = handler;
|
||||
break;
|
||||
} catch (exc) {
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: handler failed`);
|
||||
console.debug(exc);
|
||||
lastError = exc;
|
||||
}
|
||||
}
|
||||
this.error = found ? undefined : (lastError ?? "Unspecified error").toString();
|
||||
};
|
||||
this.#refreshVendor();
|
||||
}
|
||||
|
||||
public updated(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("refreshedAt") || !this.challenge) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.activeHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug("authentik/stages/captcha: refresh triggered");
|
||||
|
||||
this.#run(this.activeHandler);
|
||||
}
|
||||
|
||||
#refreshVendor() {
|
||||
this.#scriptElement?.remove();
|
||||
|
||||
const scriptElement = document.createElement("script");
|
||||
|
||||
scriptElement.src = this.challenge.jsUrl;
|
||||
scriptElement.async = true;
|
||||
scriptElement.defer = true;
|
||||
scriptElement.dataset.akCaptchaScript = "true";
|
||||
scriptElement.onload = attachCaptcha;
|
||||
scriptElement.onload = this.#scriptLoadListener;
|
||||
|
||||
document.head
|
||||
.querySelectorAll("[data-ak-captcha-script=true]")
|
||||
.forEach((el) => el.remove());
|
||||
this.#scriptElement?.remove();
|
||||
|
||||
document.head.appendChild(scriptElement);
|
||||
this.#scriptElement = document.head.appendChild(scriptElement);
|
||||
|
||||
if (!this.challenge.interactive) {
|
||||
document.body.appendChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("refreshedAt") || !this.challenge) {
|
||||
//#endregion
|
||||
|
||||
//#region Listeners
|
||||
|
||||
#loadListener = () => {
|
||||
const iframe = this.#iframeRef.value;
|
||||
const contentDocument = iframe?.contentDocument;
|
||||
|
||||
if (!iframe || !contentDocument) return;
|
||||
|
||||
const resizeListener: ResizeObserverCallback = () => {
|
||||
if (!this.#iframeRef) return;
|
||||
|
||||
const target = contentDocument.getElementById("ak-container");
|
||||
|
||||
if (!target) return;
|
||||
|
||||
this.iframeHeight = Math.round(target.clientHeight);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(resizeListener);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
resizeObserver.observe(contentDocument.body);
|
||||
this.onLoad?.();
|
||||
this.#iframeLoaded = true;
|
||||
});
|
||||
};
|
||||
|
||||
#scriptLoadListener = async (): Promise<void> => {
|
||||
console.debug("authentik/stages/captcha: script loaded");
|
||||
|
||||
this.error = null;
|
||||
this.#iframeLoaded = false;
|
||||
|
||||
for (const [name, handler] of this.#handlers) {
|
||||
if (!Object.hasOwn(window, name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#run(handler);
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
|
||||
|
||||
this.activeHandler = handler;
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler failed`);
|
||||
console.debug(error);
|
||||
|
||||
this.error = pluckErrorDetail(error, "Unspecified error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async #run(handler: CaptchaHandler) {
|
||||
if (this.challenge.interactive) {
|
||||
const iframe = this.#iframeRef.value;
|
||||
|
||||
if (!iframe) {
|
||||
console.debug(`authentik/stages/captcha: No iframe found, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`authentik/stages/captcha: Rendering interactive.`);
|
||||
|
||||
const captchaElement = handler.interactive();
|
||||
const template = iframeTemplate(captchaElement, this.challenge.jsUrl);
|
||||
|
||||
URL.revokeObjectURL(this.#iframeSource);
|
||||
|
||||
const url = URL.createObjectURL(new Blob([template], { type: "text/html" }));
|
||||
|
||||
this.#iframeSource = url;
|
||||
|
||||
iframe.src = url;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug("authentik/stages/captcha: refresh triggered");
|
||||
if (this.challenge.interactive) {
|
||||
this.activeHandler?.refreshInteractive.apply(this);
|
||||
} else {
|
||||
this.activeHandler?.refresh.apply(this);
|
||||
}
|
||||
await handler.execute.apply(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
web/src/flow/stages/captcha/grecaptcha.ts
Normal file
11
web/src/flow/stages/captcha/grecaptcha.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="@types/grecaptcha"/>
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
grecaptcha: ReCaptchaV2.ReCaptcha & {
|
||||
enterprise: ReCaptchaV2.ReCaptcha;
|
||||
};
|
||||
}
|
||||
}
|
||||
9
web/src/flow/stages/captcha/hcaptcha.ts
Normal file
9
web/src/flow/stages/captcha/hcaptcha.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="@hcaptcha/types"/>
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
hcaptcha?: HCaptcha;
|
||||
}
|
||||
}
|
||||
65
web/src/flow/stages/captcha/shared.ts
Normal file
65
web/src/flow/stages/captcha/shared.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createDocumentTemplate } from "#elements/utils/iframe";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
export interface CaptchaHandler {
|
||||
interactive(): TemplateResult;
|
||||
execute(): Promise<void>;
|
||||
refreshInteractive(): Promise<void>;
|
||||
refresh(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A container iframe for a hosted Captcha, with an event emitter to monitor
|
||||
* when the Captcha forces a resize.
|
||||
*
|
||||
* Because the Captcha is itself in an iframe, the reported height is often off by some
|
||||
* margin, adding 2rem of height to our container adds padding and prevents scrollbars
|
||||
* or hidden rendering.
|
||||
*/
|
||||
export function iframeTemplate(children: TemplateResult, challengeURL: string): string {
|
||||
return createDocumentTemplate({
|
||||
head: html`<meta charset="UTF-8" />
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
function callback(token) {
|
||||
self.parent.postMessage({
|
||||
message: "captcha",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor",
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
function loadListener() {
|
||||
self.parent.postMessage({
|
||||
message: "load",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.g-recaptcha {
|
||||
padding-block: 0.5rem;
|
||||
}
|
||||
|
||||
.g-recaptcha,
|
||||
.h-captcha {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>`,
|
||||
body: html`${children}
|
||||
<script onload="loadListener()" src="${challengeURL}"></script> `,
|
||||
});
|
||||
}
|
||||
9
web/src/flow/stages/captcha/turnstile.ts
Normal file
9
web/src/flow/stages/captcha/turnstile.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
/// <reference types="turnstile-types"/>
|
||||
import { TurnstileObject } from "turnstile-types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
turnstile: TurnstileObject;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { AkRememberMeController } from "@goauthentik/flow/stages/identification/
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
@@ -54,6 +55,8 @@ export class IdentificationStage extends BaseStage<
|
||||
captchaToken = "";
|
||||
@state()
|
||||
captchaRefreshedAt = new Date();
|
||||
@state()
|
||||
captchaLoaded = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
@@ -81,16 +84,42 @@ export class IdentificationStage extends BaseStage<
|
||||
height: 100%;
|
||||
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
|
||||
}
|
||||
|
||||
.captcha-container {
|
||||
position: relative;
|
||||
|
||||
.faux-input {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
#captchaInputRef = createRef<HTMLInputElement>();
|
||||
|
||||
#tokenChangeListener = (token: string) => {
|
||||
const input = this.#captchaInputRef.value;
|
||||
|
||||
if (!input) return;
|
||||
|
||||
input.value = token;
|
||||
};
|
||||
|
||||
#captchaLoadListener = () => {
|
||||
this.captchaLoaded = true;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.rememberMe = new AkRememberMeController(this);
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
public updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("challenge") && this.challenge !== undefined) {
|
||||
this.autoRedirect();
|
||||
this.createHelperForm();
|
||||
@@ -162,9 +191,12 @@ export class IdentificationStage extends BaseStage<
|
||||
input.focus();
|
||||
});
|
||||
};
|
||||
|
||||
this.form.appendChild(password);
|
||||
}
|
||||
|
||||
const totp = document.createElement("input");
|
||||
|
||||
totp.setAttribute("type", "text");
|
||||
totp.setAttribute("name", "code");
|
||||
totp.setAttribute("autocomplete", "one-time-code");
|
||||
@@ -298,19 +330,33 @@ export class IdentificationStage extends BaseStage<
|
||||
${this.renderNonFieldErrors()}
|
||||
${this.challenge.captchaStage
|
||||
? html`
|
||||
<input name="captchaToken" type="hidden" .value="${this.captchaToken}" />
|
||||
<ak-stage-captcha
|
||||
.challenge=${this.challenge.captchaStage}
|
||||
.onTokenChange=${(token: string) => {
|
||||
this.captchaToken = token;
|
||||
}}
|
||||
.refreshedAt=${this.captchaRefreshedAt}
|
||||
embedded
|
||||
></ak-stage-captcha>
|
||||
<div class="captcha-container">
|
||||
<ak-stage-captcha
|
||||
.challenge=${this.challenge.captchaStage}
|
||||
.onTokenChange=${this.#tokenChangeListener}
|
||||
.onLoad=${this.#captchaLoadListener}
|
||||
.refreshedAt=${this.captchaRefreshedAt}
|
||||
embedded
|
||||
>
|
||||
</ak-stage-captcha>
|
||||
<input
|
||||
class="faux-input"
|
||||
${ref(this.#captchaInputRef)}
|
||||
name="captchaToken"
|
||||
type="text"
|
||||
required
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
||||
<div class="pf-c-form__group ${this.challenge.captchaStage ? "" : "pf-m-action"}">
|
||||
<button
|
||||
?disabled=${this.challenge.captchaStage && !this.captchaLoaded}
|
||||
type="submit"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>
|
||||
${this.challenge.primaryAction}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { SubmitOptions } from "#flow/stages/base";
|
||||
import { FlowExecutor } from "@goauthentik/flow/FlowExecutor";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { FlowChallengeResponseRequest } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-storybook-interface-flow")
|
||||
export class StoryFlowInterface extends FlowExecutor {}
|
||||
export class StoryFlowInterface extends FlowExecutor {
|
||||
async firstUpdated() {}
|
||||
|
||||
submit = async (
|
||||
payload?: FlowChallengeResponseRequest,
|
||||
options?: SubmitOptions,
|
||||
): Promise<boolean> => {
|
||||
return true;
|
||||
};
|
||||
|
||||
async renderChallenge(): Promise<TemplateResult> {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { StageHost } from "#flow/stages/base";
|
||||
import "#user/user-settings/details/stages/prompt/PromptStage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
@@ -36,7 +36,7 @@ export class UserSettingsFlowExecutor
|
||||
implements StageHost
|
||||
{
|
||||
@property()
|
||||
flowSlug?: string;
|
||||
flowSlug = this.brand?.flowUserSettings;
|
||||
|
||||
private _challenge?: ChallengeTypes;
|
||||
|
||||
@@ -86,12 +86,15 @@ export class UserSettingsFlowExecutor
|
||||
});
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("brand") && this.brand) {
|
||||
firstUpdated() {
|
||||
if (this.flowSlug) {
|
||||
this.nextChallenge();
|
||||
}
|
||||
}
|
||||
|
||||
updated(): void {
|
||||
if (!this.flowSlug && this.brand?.flowUserSettings) {
|
||||
this.flowSlug = this.brand.flowUserSettings;
|
||||
|
||||
if (!this.flowSlug) return;
|
||||
|
||||
this.nextChallenge();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9106,9 +9106,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9246,6 +9243,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -7608,9 +7608,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -7748,6 +7745,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9167,9 +9167,6 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9307,6 +9304,18 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9690,10 +9690,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<source>Failed to preview prompt</source>
|
||||
<target>Échec de la prévisualisation de l'invite</target>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
<target>Recherche avec un attribut utilisateur</target>
|
||||
@@ -9877,6 +9873,18 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9690,10 +9690,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Failed to preview prompt</source>
|
||||
<target>Impossibile visualizzare l'anteprima del prompt</target>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
<target>Ricerca tramite attributo utente</target>
|
||||
@@ -9860,6 +9856,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9075,9 +9075,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9215,6 +9212,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -8977,9 +8977,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9117,6 +9114,18 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9402,9 +9402,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9542,6 +9539,18 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9409,9 +9409,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9550,4 +9547,16 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body></file></xliff>
|
||||
|
||||
@@ -9494,9 +9494,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9634,6 +9631,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9465,9 +9465,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9605,6 +9602,18 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -6215,9 +6215,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -6356,6 +6353,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
||||
<body>
|
||||
<trans-unit id="s4caed5b7a7e5d89b">
|
||||
@@ -596,9 +596,9 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="saa0e2675da69651b">
|
||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||
<target>未找到 URL "
|
||||
<x id="0" equiv-text="${this.url}"/>"。</target>
|
||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||
<target>未找到 URL "
|
||||
<x id="0" equiv-text="${this.url}"/>"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s58cd9c2fe836d9c6">
|
||||
@@ -1715,8 +1715,8 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa90b7809586c35ce">
|
||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||
<target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
|
||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||
<target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s0410779cb47de312">
|
||||
@@ -3778,10 +3778,10 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa95a538bfbb86111">
|
||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||
<target>您确定要更新
|
||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
|
||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc92d7cfb6ee1fec6">
|
||||
@@ -4847,7 +4847,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sdf1d8edef27236f0">
|
||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||
<target>像 YubiKey 这样的“漫游”身份验证器</target>
|
||||
|
||||
</trans-unit>
|
||||
@@ -5206,7 +5206,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
<target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target>
|
||||
|
||||
</trans-unit>
|
||||
@@ -7492,7 +7492,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s824e0943a7104668">
|
||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||
<target>此用户将会被添加到组 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s62e7f6ed7d9cb3ca">
|
||||
@@ -8778,7 +8778,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>同步组</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2d5f69929bb7221d">
|
||||
<source><x id="0" equiv-text="${p.name}"/> ("<x id="1" equiv-text="${p.fieldKey}"/>", of type <x id="2" equiv-text="${p.type}"/>)</source>
|
||||
<source><x id="0" equiv-text="${p.name}"/> ("<x id="1" equiv-text="${p.fieldKey}"/>", of type <x id="2" equiv-text="${p.type}"/>)</source>
|
||||
<target><x id="0" equiv-text="${p.name}"/>(&quot;<x id="1" equiv-text="${p.fieldKey}"/>&quot;,类型为 <x id="2" equiv-text="${p.type}"/>)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25bacc19d98b444e">
|
||||
@@ -9026,8 +9026,8 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>授权流程成功后有效的重定向 URI。还可以在此处为隐式流程指定任何来源。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4c49d27de60a532b">
|
||||
<source>To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.</source>
|
||||
<target>要允许任何重定向 URI,请设置模式为正则表达式,并将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
|
||||
<source>To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.</source>
|
||||
<target>要允许任何重定向 URI,请设置模式为正则表达式,并将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa52bf79fe1ccb13e">
|
||||
<source>Federated OIDC Sources</source>
|
||||
@@ -9691,10 +9691,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Failed to preview prompt</source>
|
||||
<target>预览输入失败</target>
|
||||
</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>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'。当选中“使用用户属性查询”时,此配置应该为用户属性,否则为组属性。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1d47b4f61ca53e8e">
|
||||
<source>Lookup using user attribute</source>
|
||||
<target>使用用户属性查询</target>
|
||||
@@ -9784,7 +9780,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>在 authorization_code 令牌请求流程期间,如何执行身份验证</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s844baf19a6c4a9b4">
|
||||
<source>Enable "Remember me on this device"</source>
|
||||
<source>Enable "Remember me on this device"</source>
|
||||
<target>启用“在此设备上记住我”</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfa72bca733f40692">
|
||||
@@ -9878,7 +9874,19 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
|
||||
<target>删除之前由此源提供,但现已缺失的用户和组。</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>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
||||
@@ -7308,9 +7308,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -7448,6 +7445,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -9052,9 +9052,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="sc7524ea24eeeb019">
|
||||
<source>Failed to preview prompt</source>
|
||||
</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">
|
||||
<source>Lookup using user attribute</source>
|
||||
</trans-unit>
|
||||
@@ -9192,6 +9189,18 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se3b26b762110bda0">
|
||||
<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>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user