Compare commits

..

6 Commits

Author SHA1 Message Date
a351565ff0 website: Refine calculator. 2025-05-28 15:22:56 +02:00
d583ddf0a3 website/docs: add scaling page
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

fix typing, re-style a bit, fix some react errors

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

disable autocomplete

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

minor grammar fixes

Signed-off-by: Fletcher Heisler <fheisler@users.noreply.github.com>

try to fix stuff

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-05-27 19:25:32 +02:00
0d18c1d797 web: fix regression in subpath support (#14646)
* web: fix regression in subpath support, part 1

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix media path in subpath

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-27 18:42:47 +02:00
e905dd52d8 lib/sync/outgoing: sync in parallel (#14697)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-05-27 15:26:43 +02:00
245126a1c3 core, web: update translations (#14707)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-05-27 11:32:31 +00:00
15d84d30ba tests/e2e: fix flaky SAML Source test (#14708) 2025-05-27 13:18:03 +02:00
30 changed files with 651 additions and 57 deletions

View File

@ -1,6 +1,7 @@
from collections.abc import Callable
from dataclasses import asdict
from celery import group
from celery.exceptions import Retry
from celery.result import allow_join_result
from django.core.paginator import Paginator
@ -82,21 +83,41 @@ class SyncTasks:
self.logger.debug("Failed to acquire sync lock, skipping", provider=provider.name)
return
try:
for page in users_paginator.page_range:
messages.append(_("Syncing page {page} of users".format(page=page)))
for msg in sync_objects.apply_async(
args=(class_to_path(User), page, provider_pk),
time_limit=PAGE_TIMEOUT,
soft_time_limit=PAGE_TIMEOUT,
).get():
messages.append(_("Syncing users"))
user_results = (
group(
[
sync_objects.signature(
args=(class_to_path(User), page, provider_pk),
time_limit=PAGE_TIMEOUT,
soft_time_limit=PAGE_TIMEOUT,
)
for page in users_paginator.page_range
]
)
.apply_async()
.get()
)
for result in user_results:
for msg in result:
messages.append(LogEvent(**msg))
for page in groups_paginator.page_range:
messages.append(_("Syncing page {page} of groups".format(page=page)))
for msg in sync_objects.apply_async(
args=(class_to_path(Group), page, provider_pk),
time_limit=PAGE_TIMEOUT,
soft_time_limit=PAGE_TIMEOUT,
).get():
messages.append(_("Syncing groups"))
group_results = (
group(
[
sync_objects.signature(
args=(class_to_path(Group), page, provider_pk),
time_limit=PAGE_TIMEOUT,
soft_time_limit=PAGE_TIMEOUT,
)
for page in groups_paginator.page_range
]
)
.apply_async()
.get()
)
for result in group_results:
for msg in result:
messages.append(LogEvent(**msg))
except TransientSyncException as exc:
self.logger.warning("transient sync exception", exc=exc)
@ -132,6 +153,15 @@ class SyncTasks:
self.logger.debug("starting discover")
client.discover()
self.logger.debug("starting sync for page", page=page)
messages.append(
asdict(
LogEvent(
_("Syncing page {page} of groups".format(page=page)),
log_level="info",
logger=f"{provider._meta.verbose_name}@{object_type}",
)
)
)
for obj in paginator.page(page).object_list:
obj: Model
try:

View File

@ -384,7 +384,7 @@ class SCIMUserTests(TestCase):
self.assertIn(request.method, SAFE_METHODS)
task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first()
self.assertIsNotNone(task)
drop_msg = task.messages[2]
drop_msg = task.messages[3]
self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run")
self.assertIsNotNone(drop_msg["attributes"]["url"])
self.assertIsNotNone(drop_msg["attributes"]["body"])

View File

@ -424,7 +424,7 @@ else:
"BACKEND": "authentik.root.storages.FileStorage",
"OPTIONS": {
"location": Path(CONFIG.get("storage.media.file.path")),
"base_url": "/media/",
"base_url": CONFIG.get("web.path", "/") + "media/",
},
}
# Compatibility for apps not supporting top-level STORAGES

View File

@ -31,6 +31,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
if kwargs.get("randomly_seed", None):
self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
if kwargs.get("no_capture", False):
self.args.append("--capture=no")
settings.TEST = True
settings.CELERY["task_always_eager"] = True
@ -64,6 +66,11 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
"Default behaviour: use random.Random().getrandbits(32), so the seed is"
"different on each run.",
)
parser.add_argument(
"--no-capture",
action="store_true",
help="Disable any capturing of stdout/stderr during tests.",
)
def run_tests(self, test_labels, extra_tests=None, **kwargs):
"""Run pytest and return the exitcode.

View File

@ -168,13 +168,10 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
user__pk=self.user.pk,
).first()
self.assertIsNotNone(event)
context = dict(event.context)
# The auth_method field is being obfuscated as it's marked as sensitive in Django 5.2
auth_method = context.pop("auth_method")
self.assertIn(auth_method, ["auth_mfa", "********************"])
self.assertEqual(
context,
event.context,
{
"auth_method": "auth_mfa",
"auth_method_args": {
"mfa_devices": [
{

View File

@ -67,11 +67,15 @@ func (ws *WebServer) configureStatic() {
// Media files, if backend is file
if config.Get().Storage.Media.Backend == "file" {
fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path)))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
fsMedia.ServeHTTP(w, r)
})
fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))
indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/media/").Handler(pathStripper(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
fsMedia.ServeHTTP(w, r)
}),
"media/",
config.Get().Web.Path,
))
}
staticRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/help/").Handler(pathStripper(

View File

@ -13,7 +13,7 @@ dependencies = [
"dacite==1.9.2",
"deepmerge==2.0",
"defusedxml==0.7.1",
"django==5.2.1",
"django==5.1.9",
"django-countries==7.6.1",
"django-cte==1.3.3",
"django-filter==25.1",

View File

@ -1,5 +1,6 @@
services:
chrome:
platform: linux/x86_64
image: docker.io/selenium/standalone-chrome:136.0
volumes:
- /dev/shm:/dev/shm

View File

@ -166,30 +166,35 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
print("::group::authentik Logs", file=stderr)
apps.get_app_config("authentik_tenants").ready()
self.wait_timeout = 60
self.logger = get_logger()
self.driver = self._get_driver()
self.driver.implicitly_wait(30)
self.wait = WebDriverWait(self.driver, self.wait_timeout)
self.logger = get_logger()
self.user = create_test_admin_user()
super().setUp()
def _get_driver(self) -> WebDriver:
count = 0
try:
opts = webdriver.ChromeOptions()
opts.add_argument("--disable-search-engine-choice-screen")
return webdriver.Chrome(options=opts)
except WebDriverException:
pass
opts = webdriver.ChromeOptions()
opts.add_argument("--disable-search-engine-choice-screen")
# This breaks selenium when running remotely...?
# opts.set_capability("goog:loggingPrefs", {"browser": "ALL"})
opts.add_experimental_option(
"prefs",
{
"profile.password_manager_leak_detection": False,
},
)
while count < RETRIES:
try:
driver = webdriver.Remote(
command_executor="http://localhost:4444/wd/hub",
options=webdriver.ChromeOptions(),
options=opts,
)
driver.maximize_window()
return driver
except WebDriverException:
except WebDriverException as exc:
self.logger.warning("Failed to setup webdriver", exc=exc)
count += 1
raise ValueError(f"Webdriver failed after {RETRIES}.")

8
uv.lock generated
View File

@ -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.2.1" },
{ name = "django", specifier = "==5.1.9" },
{ name = "django-countries", specifier = "==7.6.1" },
{ name = "django-cte", specifier = "==1.3.3" },
{ name = "django-filter", specifier = "==25.1" },
@ -979,16 +979,16 @@ wheels = [
[[package]]
name = "django"
version = "5.2.1"
version = "5.1.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/10/0d546258772b8f31398e67c85e52c66ebc2b13a647193c3eef8ee433f1a8/django-5.2.1.tar.gz", hash = "sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284", size = 10818735, upload-time = "2025-05-07T14:06:17.543Z" }
sdist = { url = "https://files.pythonhosted.org/packages/10/08/2e6f05494b3fc0a3c53736846034f882b82ee6351791a7815bbb45715d79/django-5.1.9.tar.gz", hash = "sha256:565881bdd0eb67da36442e9ac788bda90275386b549070d70aee86327781a4fc", size = 10710887, upload-time = "2025-05-07T14:06:45.257Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/92/7448697b5838b3a1c6e1d2d6a673e908d0398e84dc4f803a2ce11e7ffc0f/django-5.2.1-py3-none-any.whl", hash = "sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961", size = 8301833, upload-time = "2025-05-07T14:06:10.955Z" },
{ url = "https://files.pythonhosted.org/packages/e1/d1/d8b6b8250b84380d5a123e099ad3298a49407d81598faa13b43a2c6d96d7/django-5.1.9-py3-none-any.whl", hash = "sha256:2fd1d4a0a66a5ba702699eb692e75b0d828b73cc2f4e1fc4b6a854a918967411", size = 8277363, upload-time = "2025-05-07T14:06:37.426Z" },
]
[[package]]

View File

@ -6,7 +6,6 @@
*/
import { mdxPlugin } from "#bundler/mdx-plugin/node";
import { createBundleDefinitions } from "#bundler/utils/node";
import { DistDirectoryName } from "#paths";
import { DistDirectory, EntryPoint, PackageRoot } from "#paths/node";
import { NodeEnvironment } from "@goauthentik/core/environment/node";
import { MonoRepoRoot, resolvePackage } from "@goauthentik/core/paths/node";
@ -29,7 +28,6 @@ const BASE_ESBUILD_OPTIONS = {
entryNames: `[dir]/[name]-${readBuildIdentifier()}`,
chunkNames: "[dir]/chunks/[hash]",
assetNames: "assets/[dir]/[name]-[hash]",
publicPath: path.join("/static", DistDirectoryName),
outdir: DistDirectory,
bundle: true,
write: true,

View File

@ -8573,7 +8573,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -7099,7 +7099,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -8652,7 +8652,7 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<target>Aplicaciones externas que utilizan <x id="0" equiv-text="${this.brand.brandingTitle || &quot;authentik&quot;}"/> como proveedor de identidad a través de protocolos como OAuth2 y SAML. Aquí se muestran todas las aplicaciones, incluso aquellas a las que no puede acceder.</target>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">

View File

@ -9009,7 +9009,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<target>Cette option configure les liens affichés en bas de page sur lexécuteur de flux. L'URL est limitée à des addresses web et courriel. Si le nom est laissé vide, l'URL sera affichée.</target>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<target>Applications externes qui utilisent <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> comme fournisseur d'identité en utilisant des protocoles comme OAuth2 et SAML. Toutes les applications sont affichées ici, même celles auxquelles vous n'avez pas accès.</target>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">

View File

@ -9010,7 +9010,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>Questo opzione configura il link in basso nel flusso delle pagine di esecuzione. L'URL e' limitato a web e indirizzo mail-Se il nome viene lasciato vuoto, verra' visualizzato l'URL</target>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<target>Applicazioni esterne che utilizzano <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> come fornitore di identità tramite protocolli come OAuth2 e SAML. Qui sono mostrate tutte le applicazioni, anche quelle a cui non è possibile accedere.</target>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">

View File

@ -8565,7 +8565,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -8467,7 +8467,7 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -8892,7 +8892,7 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -8900,7 +8900,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -8942,7 +8942,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -8955,7 +8955,7 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -5706,7 +5706,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -9010,7 +9010,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>此选项配置流程执行器页面上的页脚链接。URL 限为 Web 和电子邮件地址。如果名称留空,则显示 URL 自身。</target>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<target>通过 OAuth2 和 SAML 等协议,使用 <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> 作为身份提供程序的外部应用程序。此处显示了所有应用程序,即使您无法访问的也包括在内。</target>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">

View File

@ -6799,7 +6799,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -8542,7 +8542,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand?.brandingTitle ?? &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<source>External applications that use <x id="0" equiv-text="${this.brandingTitle}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
</trans-unit>
<trans-unit id="s58bec0ecd4f3ccd4">
<source>Strict</source>

View File

@ -0,0 +1,50 @@
---
title: Scaling
---
import ScalingCalculator from "@site/src/components/ScalingCalculator";
authentik can be scaled to several thousands of users. This page aims to provide some guidance on installation sizing and tuning for those large installations.
:::note
The numbers indicated on this page are meant as general guidelines and starting points. Usage patterns vary from one installation to another, and we are constantly optimizing authentik to improve its performance.
The data used to build this page has been collected from benchmarks run on version 2024.6, and from real-world customer feedback.
:::
## How does authentik scale?
### authentik server
The authentik server is a stateless component that serves HTTP requests. As such, it is completely stateless and scales linearly. That means that if a single authentik server with X resources (CPU and RAM) can serve N requests, two authentik server with each X resources can serve 2N requests.
### authentik worker
The authentik worker is a stateless component that processes background tasks for authentik. This includes:
- maintenance tasks, such as removing outdated resources
- synchronization tasks for sources and providers such as SCIM, Google Workspace, LDAP, etc.
- one-off tasks that are triggered by some action by a user or an administrator, such as sending notifications.
Scaling the worker thus greatly depends on the usage made of authentik. If not synchronization is configured and there are few one-off tasks, then the worker will use almost no resources. However, if the worker has to process lengthy synchronization tasks and is then backlogged with other tasks, then it needs to be scaled up.
As such, we recommend setting up some [monitoring](../sys-mgmt/monitoring.md) and observing how the worker behaves, then scaling it up accordingly. You should aim to have as few tasks as possible in the worker queue.
### PostgreSQL & Redis
We recommend looking into both tools' respective documentation to get pointers on how to monitor them.
The specific usage that authentik makes of PostgreSQL and Redis doesn't scale linearly, but rather logarithmically.
## Calculator
<ScalingCalculator />
#### Known issues
The database recommendations given by the calculator are scaled linearly to the number of concurrent logins. This is not the pattern we observe in reality. As such, if the calculator returns ludicrous values for the database sizing, expect those to not be representative of the actual resources needed.
## Feedback
We welcome feedback on this calculator. [Contact us!](mailto:hello@goauthentik.io)

View File

@ -123,6 +123,7 @@ const items = [
"install-config/reverse-proxy",
"install-config/automated-install",
"install-config/air-gapped",
"install-config/scaling",
],
},
{

View File

@ -0,0 +1,374 @@
import Link from "@docusaurus/Link";
import Translate from "@docusaurus/Translate";
import Admonition from "@theme/Admonition";
import React, { useCallback, useMemo, useState } from "react";
import styles from "./style.module.css";
type RowID =
| "replicas"
| "requests_cpu"
| "requests_memory"
| "gunicorn_workers"
| "gunicorn_threads";
const rows: [rowID: RowID, rowLabel: JSX.Element, units?: string][] = [
[
// ---
"replicas",
<Translate id="ak.scalingCalculator.replicas">Replicas</Translate>,
],
[
// ---
"requests_cpu",
<Translate id="ak.scalingCalculator.requestsCpu">CPU Requests</Translate>,
],
[
// ---
"requests_memory",
<Translate id="ak.scalingCalculator.requestsMemory">Memory Requests</Translate>,
"GB",
],
[
// ---
"gunicorn_workers",
<Link to="./configuration#authentik_web__workers">
<Translate>Gunicorn Workers</Translate>
</Link>,
],
[
// ---
"gunicorn_threads",
<Link to="./configuration#authentik_web__threads">
<Translate>Gunicorn Threads</Translate>
</Link>,
],
];
type SetupEstimate = {
[key in RowID]: number;
};
type SetupEntry = [columnLabel: React.ReactNode, estimate: SetupEstimate];
const FieldName = {
UserCount: "userCount",
ConcurrentLogins: "loginCount",
FlowDuration: "flowDuration",
} as const satisfies Record<string, string>;
type FieldKey = keyof typeof FieldName;
type FieldName = (typeof FieldName)[FieldKey];
type EstimateInput = { [key in FieldName]: number };
type FieldID = `${FieldName}-field`;
const FieldID = Object.fromEntries(
Object.entries(FieldName).map(([key, value]) => [key, `${value}-field`]),
) as Record<FieldKey, FieldID>;
const SetupComparisionTable: React.FC<EstimateInput> = ({ loginCount }) => {
const cpuCount = Math.max(1, Math.ceil(loginCount / 10));
const setups: SetupEntry[] = [
[
<Translate
id="ak.setup.kubernetesRAMOptimized"
values={{ platform: "Kubernetes", variant: "(RAM Optimized)", separator: <br /> }}
>
{"{platform}{separator}{variant}"}
</Translate>,
{
gunicorn_threads: 2,
gunicorn_workers: 3,
replicas: Math.max(2, Math.ceil(cpuCount / 2)),
requests_cpu: 2,
requests_memory: 1.5,
},
],
[
<Translate
id="ak.setup.kubernetesCPUOptimized"
values={{ platform: "Kubernetes", variant: "(CPU Optimized)", separator: <br /> }}
>
{"{platform}{separator}{variant}"}
</Translate>,
{
gunicorn_threads: 2,
gunicorn_workers: 2,
replicas: Math.max(2, cpuCount),
requests_cpu: 1,
requests_memory: 1,
},
],
[
<Translate
id="ak.setup.dockerVM"
values={{
platform: "Docker Compose",
variant: "(Virtual machine)",
separator: <br />,
}}
>
{"{platform}{separator}{variant}"}
</Translate>,
{
gunicorn_threads: 2,
gunicorn_workers: cpuCount + 1,
replicas: Math.max(2, cpuCount),
requests_cpu: cpuCount,
requests_memory: cpuCount,
},
],
];
return (
<Admonition type="tip" icon={null} title={null} className={styles.admonitionTable}>
<div
className={styles.comparisionTable}
style={
{ "--ak-comparision-table-columns": setups.length + 1 } as React.CSSProperties
}
>
<header>
<div className={styles.columnLabel}>
<Translate id="ak.scalingCalculator.server">Resources</Translate>
</div>
{setups.map(([columnLabel], i) => (
<div className={styles.columnLabel} key={i}>
{columnLabel}
</div>
))}
</header>
{rows.map(([rowID, rowLabel, units]) => {
return (
<section key={rowID}>
<div className={styles.rowLabel}>{rowLabel}</div>
{setups.map(([_rowLabel, estimate], i) => {
const estimateValue = estimate[rowID] || "N/A";
return (
<div className={styles.fieldValue} key={i}>
<Translate
id={`ak.scalingCalculator.${rowID}`}
values={{
value: estimateValue,
units: units ? ` ${units}` : "",
}}
>
{"{value}{units}"}
</Translate>
</div>
);
})}
</section>
);
})}
</div>
</Admonition>
);
};
export const DatabaseEstimateTable: React.FC<EstimateInput> = ({ loginCount, userCount }) => {
const cpuCount = Math.max(1, Math.ceil(loginCount / 10));
const postgres = {
cpus: Math.max(2, cpuCount / 4),
ram: Math.max(4, cpuCount),
storage_gb: Math.ceil(userCount / 25000),
};
const redis = {
cpus: Math.max(2, cpuCount / 4),
ram: Math.max(2, cpuCount / 2),
};
return (
<Admonition type="tip" icon={null} title={null} className={styles.admonitionTable}>
<div
className={styles.comparisionTable}
style={{ "--ak-comparision-table-columns": 3 } as React.CSSProperties}
>
<header>
<div className={styles.columnLabel}>
<Translate id="ak.scalingCalculator.server">Resources</Translate>
</div>
<div className={styles.columnLabel}>PostgreSQL</div>
<div className={styles.columnLabel}>Redis</div>
</header>
<section>
<div className={styles.rowLabel}>CPUs</div>
<div className={styles.fieldValue}>{postgres.cpus}</div>
<div className={styles.fieldValue}>{redis.cpus}</div>
</section>
<section>
<div className={styles.rowLabel}>Memory</div>
<div className={styles.fieldValue}>{postgres.ram} GB</div>
<div className={styles.fieldValue}>{redis.ram} GB</div>
</section>
<section>
<div className={styles.rowLabel}>Storage</div>
<div className={styles.fieldValue}>{postgres.storage_gb} GB</div>
<div className={styles.fieldValue}>
<Translate id="ak.scalingCalculator.varies">Varies</Translate>
</div>
<div />
</section>
</div>
</Admonition>
);
};
export const ScalingCalculator: React.FC = () => {
const [estimateInput, setEstimateInput] = useState<EstimateInput>(() => {
const userCount = 100;
const flowDuration = 15;
return {
userCount,
flowDuration,
loginCount: -1,
};
});
const estimatedLoginCount = useMemo(() => {
const { userCount, flowDuration } = estimateInput;
// if (loginCount > 0) return loginCount;
// Assumption that users log in over a period of 15 minutes.
return Math.ceil(userCount / 15.0 / 60.0) * flowDuration;
}, [estimateInput]);
const estimatedLoginValue = estimateInput[FieldName.ConcurrentLogins];
const handleFieldChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
const { name, value } = event.target;
const nextFieldValue = value.length ? parseInt(value, 10) : -1;
setEstimateInput((currentEstimate) => ({
...currentEstimate,
[name as FieldName]: nextFieldValue,
}));
}, []);
return (
<>
<h3>
<Translate id="ak.scalingCalculator.usageEstimates">Usage Estimates</Translate>
</h3>
<Admonition type="info" icon={null} title={null}>
<form className={styles.admonitionForm} autoComplete="off">
<div className={styles.labelGroup}>
<label htmlFor={FieldID.UserCount}>
<Translate id="ak.scalingCalculator.activeUsersLabel">
Active Users
</Translate>
</label>
<p>
<Translate id="ak.scalingCalculator.activeUsersDescription">
This is used to calculate database storage, and estimate how many
concurrent logins you can expect.
</Translate>
</p>
</div>
<div className={styles.field}>
<input
id={FieldID.UserCount}
type="number"
step="10"
name={FieldName.UserCount}
value={estimateInput[FieldName.UserCount]}
onChange={handleFieldChange}
required
min={1}
/>
</div>
<div className={styles.labelGroup}>
<label htmlFor={FieldID.FlowDuration}>
<Translate id="ak.scalingCalculator.flowDurationLabel">
Flow Duration
</Translate>
</label>
<p>
<Translate id="ak.scalingCalculator.flowDurationDescription">
A single login may take several seconds for the user to enter their
password, MFA method, etc. If you know what usage pattern to expect,
you can override that value from the computed one.
</Translate>
</p>
</div>
<div className={styles.field}>
<input
id={FieldID.FlowDuration}
type="number"
step="5"
name={FieldName.FlowDuration}
value={estimateInput[FieldName.FlowDuration]}
onChange={handleFieldChange}
min={0}
/>
</div>
<div className={styles.labelGroup}>
<label htmlFor={FieldID.ConcurrentLogins}>
<Translate id="ak.scalingCalculator.concurrentLoginsLabel">
Concurrent Logins
</Translate>
</label>
<p>
<Translate id="ak.scalingCalculator.concurrentLoginsDescription">
We estimate that all of the users will log in over a period of 15
minutes, greatly reducing the load on the instance.
</Translate>
</p>
</div>
<div className={styles.field}>
<input
id={FieldID.ConcurrentLogins}
type="number"
step="10"
name={FieldName.ConcurrentLogins}
placeholder={estimatedLoginCount.toString()}
value={estimatedLoginValue === -1 ? "" : estimatedLoginValue.toString()}
onChange={handleFieldChange}
min={0}
/>
</div>
</form>
</Admonition>
<h3>
<Translate id="ak.scalingCalculator.deploymentConfigurations">
Deployment Configurations
</Translate>
</h3>
<SetupComparisionTable {...estimateInput} />
<h3>
<Translate id="ak.scalingCalculator.DatabaseConfigurations">
Database Configurations
</Translate>
</h3>
<DatabaseEstimateTable {...estimateInput} />
</>
);
};
export default ScalingCalculator;

View File

@ -0,0 +1,127 @@
.admonitionForm {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: var(--ifm-global-spacing);
@media (max-width: 1119px) {
grid-template-columns: 1fr;
}
.labelGroup {
color: var(--ifm-alert-color);
label {
font-weight: var(--ifm-font-weight-bold);
}
}
.field {
display: flex;
align-items: center;
position: relative;
input[type="number"] {
font-weight: var(--ifm-font-weight-bold);
font-size: 1.25em;
flex: 1 1 auto;
background-color: var(--ifm-alert-background-color-highlight);
padding: 1em;
border-color: var(--ifm-alert-background-color-highlight);
border-radius: var(--ifm-alert-border-radius);
border-style: double;
box-shadow: inset var(--ifm-global-shadow-lw);
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
}
}
&:has(input[name="flowDuration"])::after {
font-variant: all-petite-caps;
font-weight: var(--ifm-font-weight-bold);
font-size: 1.25em;
flex: 0 0 auto;
content: "sec";
padding: var(--ifm-global-spacing);
display: block;
position: absolute;
right: 0;
color: var(--ifm-color-emphasis-700);
font-family: var(--ifm-font-family-monospace);
}
}
}
.admonitionTable {
padding: 0;
overflow: hidden;
}
.comparisionTable {
display: grid;
grid-template-columns: repeat(var(--ak-comparision-table-columns), 1fr);
overflow-x: auto;
& > header {
border-bottom: 1px solid var(--ifm-alert-border-color);
}
& > header,
& > section {
display: grid;
grid-column: span var(--ak-comparision-table-columns);
grid-template-columns: subgrid;
}
& > section:not(:last-child) {
border-block-end: 1px solid var(--ifm-alert-background-color-highlight);
}
& > section:nth-child(odd) {
/* background-color: var(--ifm-table-stripe-background); */
.rowLabel,
.fieldValue {
background-color: var(--ifm-table-stripe-background);
}
}
.columnLabel {
background-color: var(--ifm-alert-background-color-highlight);
font-weight: var(--ifm-table-head-font-weight);
padding: var(--ifm-table-cell-padding) calc(var(--ifm-spacing-horizontal) * 2);
text-align: center;
font-weight: var(--ifm-font-weight-bold);
place-content: center;
}
.rowLabel {
position: sticky;
left: 0;
z-index: var(--ifm-z-index-dropdown);
backdrop-filter: blur(4px);
overflow: hidden;
border-block-end-width: 4px;
}
.rowLabel,
.fieldValue {
background-color: var(--ifm-alert-background-color);
padding: var(--ifm-table-cell-padding) calc(var(--ifm-spacing-horizontal) * 2);
text-align: center;
font-weight: var(--ifm-font-weight-bold);
place-content: center;
&:not(:last-child) {
border-inline-end: 1px solid var(--ifm-alert-background-color);
}
}
.fieldValue {
font-size: 1.25em;
padding: var(--ifm-table-cell-padding);
place-items: center;
}
}