Compare commits
6 Commits
dependabot
...
website-do
Author | SHA1 | Date | |
---|---|---|---|
a351565ff0 | |||
d583ddf0a3 | |||
0d18c1d797 | |||
e905dd52d8 | |||
245126a1c3 | |||
15d84d30ba |
@ -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:
|
||||
|
@ -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"])
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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": [
|
||||
{
|
||||
|
@ -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(
|
||||
|
@ -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",
|
||||
|
@ -1,5 +1,6 @@
|
||||
services:
|
||||
chrome:
|
||||
platform: linux/x86_64
|
||||
image: docker.io/selenium/standalone-chrome:136.0
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
|
@ -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
8
uv.lock
generated
@ -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]]
|
||||
|
@ -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,
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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 || "authentik"}"/> 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">
|
||||
|
@ -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 l’exé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 ?? "authentik"}"/> 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 ?? "authentik"}"/> 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">
|
||||
|
@ -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 ?? "authentik"}"/> 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 ?? "authentik"}"/> 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">
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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 ?? "authentik"}"/> 作为身份提供程序的外部应用程序。此处显示了所有应用程序,即使您无法访问的也包括在内。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s58bec0ecd4f3ccd4">
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
@ -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 ?? "authentik"}"/> 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>
|
||||
|
50
website/docs/install-config/scaling.mdx
Normal file
50
website/docs/install-config/scaling.mdx
Normal 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)
|
@ -123,6 +123,7 @@ const items = [
|
||||
"install-config/reverse-proxy",
|
||||
"install-config/automated-install",
|
||||
"install-config/air-gapped",
|
||||
"install-config/scaling",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
374
website/src/components/ScalingCalculator/index.tsx
Normal file
374
website/src/components/ScalingCalculator/index.tsx
Normal 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;
|
127
website/src/components/ScalingCalculator/style.module.css
Normal file
127
website/src/components/ScalingCalculator/style.module.css
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user