Compare commits

..

39 Commits

Author SHA1 Message Date
cde4e395e9 add user group creation
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-22 17:16:14 +02:00
d19c692f81 fix testcases
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:48:26 +02:00
d5d2be5672 fix duration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:28:48 +02:00
8597db59f5 fix duration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:25:53 +02:00
74fb9492bc fix duration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:25:32 +02:00
defbafb55e fix with users
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:24:21 +02:00
e2ed7391bc fix event list creation
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 13:39:42 +02:00
8dcd0dcaa9 remove multiprocess for now
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 18:45:54 +02:00
18eee1b722 rework fixtures, paralelize
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 18:31:14 +02:00
d0f6c815c3 fix
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 17:09:43 +02:00
b13eba3b0a add meaningful test for provider oauth2
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 17:08:04 +02:00
77fe4e9fe2 add group and event list
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 17:05:40 +02:00
71fe8b4fb3 Merge branch 'main' into benchmarks 2024-04-17 00:42:10 +02:00
b14cb832b2 user list: hopefully fix memory usage
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-16 23:56:32 +02:00
24b5296d88 fix timeout
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-16 23:54:03 +02:00
41b7e50bc6 typo
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-16 17:19:36 +02:00
6b750d7c59 fix fixtures idempotency
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 21:50:19 +02:00
d268c28934 allow vus count configuration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 20:31:39 +02:00
688404b6a5 allow configuring remove write endpoint
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 20:00:14 +02:00
cbd2425a5f remove useless prom args
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 16:45:38 +02:00
877c264d59 idempotent fixtures
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 16:18:31 +02:00
2575b540fa proper url tags
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 15:06:29 +02:00
0e0b76a62e fix external labels
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 14:46:50 +02:00
6d625fd1d7 support other than localhost
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 14:30:53 +02:00
bd0630e300 fix main fixtures
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 14:25:31 +02:00
ffb7d44024 config for thanos
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 13:59:16 +02:00
7589b11f98 add tests for policies
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-12 17:58:36 +02:00
ad21dfa2bc Merge branch 'main' into benchmarks 2024-04-11 19:11:27 +02:00
95692f5a7c provider oauth2 test
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-11 19:10:47 +02:00
1f4ed1defa user list: add support for with_groups
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 17:17:20 +02:00
334b183465 optimize fixtures, better user_list tests
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 17:13:24 +02:00
1f789dd4c5 more cleanup
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 16:24:44 +02:00
057e5747c9 remove custom k6 install
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 16:24:18 +02:00
8717a3aaab fix
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 14:18:59 +02:00
527173236a Merge branch 'main' into benchmarks 2024-04-09 14:17:28 +02:00
3e6eb6f248 add login tests
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 14:16:58 +02:00
6babf0f1c4 add graphs
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-08 17:29:58 +02:00
ca7cc30504 use tenants for fixtures
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-08 11:59:40 +02:00
a7cb808cad init benchmarks
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-05 05:32:40 +02:00
109 changed files with 3627 additions and 14648 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.4.0
current_version = 2024.2.2
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,8 +21,6 @@ optional_value = final
[bumpversion:file:schema.yml]
[bumpversion:file:blueprints/schema.json]
[bumpversion:file:authentik/__init__.py]
[bumpversion:file:internal/constants/constants.go]

7
.gitignore vendored
View File

@ -209,3 +209,10 @@ source_docs/
### Golang ###
/vendor/
### Benchmark ###
tests/benchmark/k6
tests/benchmark/prometheus
tests/benchmark/**/*.json
tests/benchmark/**/*.ndjson
tests/benchmark/**/*.html

View File

@ -278,3 +278,20 @@ ci-bandit: ci--meta-debug
ci-pending-migrations: ci--meta-debug
ak makemigrations --check
#########################
## Benchmark
#########################
benchmark-fixtures-create:
tests/benchmark/fixtures.py create
benchmark-run:
docker compose -f tests/benchmark/docker-compose.yml up -d
sleep 5
tests/benchmark/run.sh
benchmark-fixtures-delete:
tests/benchmark/fixtures.py delete
benchmark: benchmark-fixtures-create benchmark-run benchmark-fixtures-delete

View File

@ -18,10 +18,10 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
(.x being the latest patch release for each version)
| Version | Supported |
| --------- | --------- |
| 2023.10.x | ✅ |
| 2024.2.x | ✅ |
| Version | Supported |
| --- | --- |
| 2023.6.x | ✅ |
| 2023.8.x | ✅ |
## Reporting a Vulnerability
@ -31,12 +31,12 @@ To report a vulnerability, send an email to [security@goauthentik.io](mailto:se
authentik reserves the right to reclassify CVSS as necessary. To determine severity, we will use the CVSS calculator from NVD (https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The calculated CVSS score will then be translated into one of the following categories:
| Score | Severity |
| ---------- | -------- |
| 0.0 | None |
| 0.1 3.9 | Low |
| 4.0 6.9 | Medium |
| 7.0 8.9 | High |
| Score | Severity |
| --- | --- |
| 0.0 | None |
| 0.1 3.9 | Low |
| 4.0 6.9 | Medium |
| 7.0 8.9 | High |
| 9.0 10.0 | Critical |
## Disclosure process

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.4.0"
__version__ = "2024.2.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,21 +0,0 @@
# Generated by Django 5.0.4 on 2024-04-18 18:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0005_tenantuuid_to_branduuid"),
]
operations = [
migrations.AddIndex(
model_name="brand",
index=models.Index(fields=["domain"], name="authentik_b_domain_b9b24a_idx"),
),
migrations.AddIndex(
model_name="brand",
index=models.Index(fields=["default"], name="authentik_b_default_3ccf12_idx"),
),
]

View File

@ -84,7 +84,3 @@ class Brand(SerializerModel):
class Meta:
verbose_name = _("Brand")
verbose_name_plural = _("Brands")
indexes = [
models.Index(fields=["domain"]),
models.Index(fields=["default"]),
]

View File

@ -54,6 +54,9 @@ options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
# used_by API that allows models to specify if they shadow an object
# for example the proxy provider which is built on top of an oauth provider
"authentik_used_by_shadows",
# List fields for which changes are not logged (due to them having dedicated objects)
# for example user's password and last_login
"authentik_signals_ignored_fields",
)
@ -332,6 +335,14 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
models.Index(fields=["path"]),
models.Index(fields=["type"]),
]
authentik_signals_ignored_fields = [
# Logged by the events `password_set`
# the `password_set` action/signal doesn't currently convey which user
# initiated the password change, so for now we'll log two actions
# ("password", "password_change_date"),
# Logged by `login`
("last_login",),
]
class Provider(SerializerModel):

View File

@ -102,4 +102,9 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
new_state = self.serialize_simple(instance)
diff = self.diff(prev_state, new_state)
thread_kwargs["diff"] = diff
if not created:
ignored_field_sets = getattr(instance._meta, "authentik_signals_ignored_fields", [])
for field_set in ignored_field_sets:
if set(diff.keys()) == set(field_set):
return None
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)

View File

@ -14,7 +14,7 @@ from pathlib import Path
from sys import argv, stderr
from time import time
from typing import Any
from urllib.parse import quote_plus, urlparse
from urllib.parse import urlparse
import yaml
from django.conf import ImproperlyConfigured
@ -331,26 +331,6 @@ class ConfigLoader:
CONFIG = ConfigLoader()
def redis_url(db: int) -> str:
"""Helper to create a Redis URL for a specific database"""
_redis_protocol_prefix = "redis://"
_redis_tls_requirements = ""
if CONFIG.get_bool("redis.tls", False):
_redis_protocol_prefix = "rediss://"
_redis_tls_requirements = f"?ssl_cert_reqs={CONFIG.get('redis.tls_reqs')}"
if _redis_ca := CONFIG.get("redis.tls_ca_cert", None):
_redis_tls_requirements += f"&ssl_ca_certs={_redis_ca}"
_redis_url = (
f"{_redis_protocol_prefix}"
f"{quote_plus(CONFIG.get('redis.username'))}:"
f"{quote_plus(CONFIG.get('redis.password'))}@"
f"{quote_plus(CONFIG.get('redis.host'))}:"
f"{CONFIG.get_int('redis.port')}"
f"/{db}{_redis_tls_requirements}"
)
return _redis_url
if __name__ == "__main__":
if len(argv) < 2: # noqa: PLR2004
print(dumps(CONFIG.raw, indent=4, cls=AttrEncoder))

View File

@ -35,7 +35,6 @@ redis:
password: ""
tls: false
tls_reqs: "none"
tls_ca_cert: null
# broker:
# url: ""
@ -59,8 +58,6 @@ remote_debug: false
log_level: info
session_storage: cache
error_reporting:
enabled: false
sentry_dsn: https://151ba72610234c4c97c5bcff4e1cffd8@authentik.error-reporting.a7k.io/4504163677503489

View File

@ -5,13 +5,13 @@ import os
from collections import OrderedDict
from hashlib import sha512
from pathlib import Path
from urllib.parse import quote_plus
from celery.schedules import crontab
from django.conf import ImproperlyConfigured
from sentry_sdk import set_tag
from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.lib.config import CONFIG, redis_url
from authentik.lib.config import CONFIG
from authentik.lib.logging import get_logger_config, structlog_configure
from authentik.lib.sentry import sentry_init
from authentik.lib.utils.reflection import get_env
@ -195,15 +195,25 @@ REST_FRAMEWORK = {
},
}
_redis_protocol_prefix = "redis://"
_redis_celery_tls_requirements = ""
if CONFIG.get_bool("redis.tls", False):
_redis_protocol_prefix = "rediss://"
_redis_celery_tls_requirements = f"?ssl_cert_reqs={CONFIG.get('redis.tls_reqs')}"
_redis_url = (
f"{_redis_protocol_prefix}"
f"{quote_plus(CONFIG.get('redis.username'))}:"
f"{quote_plus(CONFIG.get('redis.password'))}@"
f"{quote_plus(CONFIG.get('redis.host'))}:"
f"{CONFIG.get_int('redis.port')}"
)
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db")),
"LOCATION": CONFIG.get("cache.url") or f"{_redis_url}/{CONFIG.get('redis.db')}",
"TIMEOUT": CONFIG.get_int("cache.timeout", 300),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
"KEY_PREFIX": "authentik_cache",
"KEY_FUNCTION": "django_tenants.cache.make_key",
"REVERSE_KEY_FUNCTION": "django_tenants.cache.reverse_key",
@ -212,15 +222,7 @@ CACHES = {
DJANGO_REDIS_SCAN_ITERSIZE = 1000
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
match CONFIG.get("session_storage", "cache"):
case "cache":
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
case "db":
SESSION_ENGINE = "django.contrib.sessions.backends.db"
case _:
raise ImproperlyConfigured(
"Invalid session_storage setting, allowed values are db and cache"
)
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_SERIALIZER = "authentik.root.sessions.pickle.PickleSerializer"
SESSION_CACHE_ALIAS = "default"
# Configured via custom SessionMiddleware
@ -274,7 +276,7 @@ CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer",
"CONFIG": {
"hosts": [CONFIG.get("channel.url") or redis_url(CONFIG.get("redis.db"))],
"hosts": [CONFIG.get("channel.url", f"{_redis_url}/{CONFIG.get('redis.db')}")],
"prefix": "authentik_channels_",
},
},
@ -374,9 +376,11 @@ CELERY = {
"beat_scheduler": "authentik.tenants.scheduler:TenantAwarePersistentScheduler",
"task_create_missing_queues": True,
"task_default_queue": "authentik",
"broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")),
"result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")),
"broker_url": CONFIG.get("broker.url")
or f"{_redis_url}/{CONFIG.get('redis.db')}{_redis_celery_tls_requirements}",
"broker_transport_options": CONFIG.get_dict_from_b64_json("broker.transport_options"),
"result_backend": CONFIG.get("result_backend.url")
or f"{_redis_url}/{CONFIG.get('redis.db')}{_redis_celery_tls_requirements}",
}
# Sentry integration

View File

@ -34,7 +34,7 @@ def mock_ad_connection(password: str) -> Connection:
"objectSid": "unique-test-group",
"objectClass": "group",
"distinguishedName": "cn=group1,ou=groups,dc=goauthentik,dc=io",
"member": ["cn=user,ou=users,dc=goauthentik,dc=io"],
"member": ["cn=user0,ou=users,dc=goauthentik,dc=io"],
},
)
# Group without SID
@ -47,7 +47,7 @@ def mock_ad_connection(password: str) -> Connection:
},
)
connection.strategy.add_entry(
"cn=user0,ou=foo,ou=users,dc=goauthentik,dc=io",
"cn=user0,ou=users,dc=goauthentik,dc=io",
{
"userPassword": password,
"sAMAccountName": "user0_sn",

View File

@ -55,7 +55,7 @@ class LDAPSyncTests(TestCase):
)
connection.assert_called_with(
connection_kwargs={
"user": "cn=user0,ou=foo,ou=users,dc=goauthentik,dc=io",
"user": "cn=user0,ou=users,dc=goauthentik,dc=io",
"password": LDAP_PASSWORD,
}
)

View File

@ -7,6 +7,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.tokens import TokenSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Token, TokenIntents, User, UserTypes
from authentik.sources.scim.models import SCIMSource
@ -26,6 +27,25 @@ class SCIMSourceSerializer(SourceSerializer):
return relative_url
return self.context["request"].build_absolute_uri(relative_url)
def create(self, validated_data):
instance: SCIMSource = super().create(validated_data)
identifier = f"ak-source-scim-{instance.pk}"
user = User.objects.create(
username=identifier,
name=f"SCIM Source {instance.name} Service-Account",
type=UserTypes.SERVICE_ACCOUNT,
)
token = Token.objects.create(
user=user,
identifier=identifier,
intent=TokenIntents.INTENT_API,
expiring=False,
managed=f"goauthentik.io/sources/scim/{instance.pk}",
)
instance.token = token
instance.save()
return instance
class Meta:
model = SCIMSource

View File

@ -1,13 +1,12 @@
"""Authentik SCIM app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikSourceSCIMConfig(ManagedAppConfig):
class AuthentikSourceSCIMConfig(AppConfig):
"""authentik SCIM Source app config"""
name = "authentik.sources.scim"
label = "authentik_sources_scim"
verbose_name = "authentik Sources.SCIM"
mountpoint = "source/scim/"
default = True

View File

@ -1,7 +1,5 @@
"""SCIM Source"""
from uuid import uuid4
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import BaseSerializer
@ -16,12 +14,6 @@ class SCIMSource(Source):
token = models.ForeignKey(Token, on_delete=models.CASCADE, null=True, default=None)
@property
def service_account_identifier(self) -> str:
if not self.pk:
self.pk = uuid4()
return f"ak-source-scim-{self.pk}"
@property
def component(self) -> str:
"""Return component used to edit this object"""

View File

@ -1,38 +0,0 @@
from django.db.models import Model
from django.db.models.signals import pre_delete, pre_save
from django.dispatch import receiver
from authentik.core.models import Token, TokenIntents, User, UserTypes
from authentik.sources.scim.models import SCIMSource
@receiver(pre_save, sender=SCIMSource)
def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
"""Create service account before source is saved"""
# .service_account_identifier will auto-assign a primary key uuid to the source
# if none is set yet, just so we can get the identifier before we save
identifier = instance.service_account_identifier
user = User.objects.create(
username=identifier,
name=f"SCIM Source {instance.name} Service-Account",
type=UserTypes.INTERNAL_SERVICE_ACCOUNT,
)
token = Token.objects.create(
user=user,
identifier=identifier,
intent=TokenIntents.INTENT_API,
expiring=False,
managed=f"goauthentik.io/sources/scim/{instance.pk}",
)
instance.token = token
@receiver(pre_delete, sender=SCIMSource)
def scim_source_pre_delete(sender: type[Model], instance: SCIMSource, **_):
"""Delete SCIM Source service account before deleting source"""
Token.objects.filter(
identifier=instance.service_account_identifier, intent=TokenIntents.INTENT_API
).delete()
User.objects.filter(
username=instance.service_account_identifier, type=UserTypes.INTERNAL_SERVICE_ACCOUNT
).delete()

View File

@ -14,13 +14,27 @@ class TestSCIMAuth(APITestCase):
def setUp(self) -> None:
self.user = create_test_admin_user()
self.token = Token.objects.create(
user=self.user,
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
)
self.token2 = Token.objects.create(
user=self.user,
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
)
self.token3 = Token.objects.create(
user=self.user,
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
)
self.source = SCIMSource.objects.create(name=generate_id(), slug=generate_id())
self.source2 = SCIMSource.objects.create(name=generate_id(), slug=generate_id())
self.source = SCIMSource.objects.create(
name=generate_id(), slug=generate_id(), token=self.token
)
self.source2 = SCIMSource.objects.create(
name=generate_id(), slug=generate_id(), token=self.token2
)
def test_auth_ok(self):
"""Test successful auth"""
@ -31,7 +45,7 @@ class TestSCIMAuth(APITestCase):
"source_slug": self.source.slug,
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)
@ -57,7 +71,7 @@ class TestSCIMAuth(APITestCase):
"source_slug": self.source.slug,
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source2.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token2.key}",
)
self.assertEqual(response.status_code, 403)
# Token for no source

View File

@ -3,6 +3,8 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Token, TokenIntents
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.sources.scim.models import SCIMSource
@ -11,9 +13,14 @@ class TestSCIMResourceTypes(APITestCase):
"""Test SCIM ResourceTypes view"""
def setUp(self) -> None:
self.user = create_test_admin_user()
self.token = Token.objects.create(
user=self.user,
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
)
self.source = SCIMSource.objects.create(
name=generate_id(),
slug=generate_id(),
name=generate_id(), slug=generate_id(), token=self.token
)
def test_resource_type(self):
@ -25,7 +32,7 @@ class TestSCIMResourceTypes(APITestCase):
"source_slug": self.source.slug,
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)
@ -39,7 +46,7 @@ class TestSCIMResourceTypes(APITestCase):
"resource_type": "ServiceProviderConfig",
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)
@ -53,6 +60,6 @@ class TestSCIMResourceTypes(APITestCase):
"resource_type": "foo",
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 404)

View File

@ -3,6 +3,8 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Token, TokenIntents
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.sources.scim.models import SCIMSource
@ -11,7 +13,15 @@ class TestSCIMSchemas(APITestCase):
"""Test SCIM Schema view"""
def setUp(self) -> None:
self.source = SCIMSource.objects.create(name=generate_id(), slug=generate_id())
self.user = create_test_admin_user()
self.token = Token.objects.create(
user=self.user,
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
)
self.source = SCIMSource.objects.create(
name=generate_id(), slug=generate_id(), token=self.token
)
def test_schema(self):
"""Test full schema view"""
@ -22,7 +32,7 @@ class TestSCIMSchemas(APITestCase):
"source_slug": self.source.slug,
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)
@ -36,7 +46,7 @@ class TestSCIMSchemas(APITestCase):
"schema_uri": "urn:ietf:params:scim:schemas:core:2.0:Meta",
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)
@ -50,6 +60,6 @@ class TestSCIMSchemas(APITestCase):
"schema_uri": "foo",
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 404)

View File

@ -3,6 +3,8 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Token, TokenIntents
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.sources.scim.models import SCIMSource
@ -11,9 +13,14 @@ class TestSCIMServiceProviderConfig(APITestCase):
"""Test SCIM ServiceProviderConfig view"""
def setUp(self) -> None:
self.user = create_test_admin_user()
self.token = Token.objects.create(
user=self.user,
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
)
self.source = SCIMSource.objects.create(
name=generate_id(),
slug=generate_id(),
name=generate_id(), slug=generate_id(), token=self.token
)
def test_config(self):
@ -25,6 +32,6 @@ class TestSCIMServiceProviderConfig(APITestCase):
"source_slug": self.source.slug,
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)

View File

@ -1,27 +0,0 @@
"""Test SCIM Source creation"""
from rest_framework.test import APITestCase
from authentik.core.models import Token, User
from authentik.lib.generators import generate_id
from authentik.sources.scim.models import SCIMSource
class TestSCIMSignals(APITestCase):
"""Test SCIM Signals view"""
def setUp(self) -> None:
self.uid = generate_id()
def test_create(self) -> None:
source = SCIMSource.objects.create(name=self.uid, slug=self.uid)
self.assertIsNotNone(source.token)
self.assertIsNotNone(source.token.user)
def test_delete(self):
self.test_create()
source = SCIMSource.objects.filter(slug=self.uid).first()
identifier = source.service_account_identifier
source.delete()
self.assertFalse(User.objects.filter(username=identifier).exists())
self.assertFalse(Token.objects.filter(identifier=identifier).exists())

View File

@ -6,8 +6,8 @@ from uuid import uuid4
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_user
from authentik.events.models import Event, EventAction
from authentik.core.models import Token, TokenIntents
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
from authentik.sources.scim.models import SCIMSource, SCIMSourceUser
@ -18,7 +18,15 @@ class TestSCIMUsers(APITestCase):
"""Test SCIM User view"""
def setUp(self) -> None:
self.source = SCIMSource.objects.create(name=generate_id(), slug=generate_id())
self.user = create_test_admin_user()
self.token = Token.objects.create(
user=self.user,
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
)
self.source = SCIMSource.objects.create(
name=generate_id(), slug=generate_id(), token=self.token
)
def test_user_list(self):
"""Test full user list"""
@ -29,16 +37,15 @@ class TestSCIMUsers(APITestCase):
"source_slug": self.source.slug,
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)
def test_user_list_single(self):
"""Test full user list (single user)"""
user = create_test_user()
SCIMSourceUser.objects.create(
source=self.source,
user=user,
user=self.user,
id=str(uuid4()),
)
response = self.client.get(
@ -46,17 +53,16 @@ class TestSCIMUsers(APITestCase):
"authentik_sources_scim:v2-users",
kwargs={
"source_slug": self.source.slug,
"user_id": str(user.uuid),
"user_id": str(self.user.uuid),
},
),
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)
SCIMUserSchema.model_validate_json(response.content, strict=True)
def test_user_create(self):
"""Test user create"""
user = create_test_user()
ext_id = generate_id()
response = self.client.post(
reverse(
@ -72,18 +78,13 @@ class TestSCIMUsers(APITestCase):
"emails": [
{
"primary": True,
"value": user.email,
"value": self.user.email,
}
],
}
),
content_type=SCIM_CONTENT_TYPE,
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 201)
self.assertTrue(SCIMSourceUser.objects.filter(source=self.source, id=ext_id).exists())
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
).exists()
)

View File

@ -20,10 +20,7 @@ class WebAuthnDeviceSerializer(ModelSerializer):
class Meta:
model = WebAuthnDevice
fields = ["pk", "name", "created_on", "device_type", "aaguid"]
extra_kwargs = {
"aaguid": {"read_only": True},
}
fields = ["pk", "name", "created_on", "device_type"]
class WebAuthnDeviceViewSet(

View File

@ -1,168 +0,0 @@
# Generated by Django 5.0.4 on 2024-04-18 11:29
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
("authentik_stages_authenticator_webauthn", "0001_initial"),
("authentik_stages_authenticator_webauthn", "0002_default_setup_flow"),
("authentik_stages_authenticator_webauthn", "0003_webauthndevice_confirmed"),
("authentik_stages_authenticator_webauthn", "0004_auto_20210304_1850"),
(
"authentik_stages_authenticator_webauthn",
"0005_authenticatewebauthnstage_user_verification",
),
(
"authentik_stages_authenticator_webauthn",
"0006_authenticatewebauthnstage_authenticator_attachment_and_more",
),
(
"authentik_stages_authenticator_webauthn",
"0007_rename_last_used_on_webauthndevice_last_t",
),
("authentik_stages_authenticator_webauthn", "0008_alter_webauthndevice_credential_id"),
("authentik_stages_authenticator_webauthn", "0009_authenticatewebauthnstage_friendly_name"),
(
"authentik_stages_authenticator_webauthn",
"0010_webauthndevicetype_authenticatorwebauthnstage_and_more",
),
("authentik_stages_authenticator_webauthn", "0011_webauthndevice_aaguid"),
]
initial = True
dependencies = [
("authentik_flows", "0016_auto_20201202_1307"),
("authentik_flows", "0027_auto_20231028_1424"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="WebAuthnDeviceType",
fields=[
("aaguid", models.UUIDField(primary_key=True, serialize=False, unique=True)),
("description", models.TextField()),
("icon", models.TextField(null=True)),
],
options={
"verbose_name": "WebAuthn Device type",
"verbose_name_plural": "WebAuthn Device types",
},
),
migrations.CreateModel(
name="AuthenticatorWebAuthnStage",
fields=[
(
"stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_flows.stage",
),
),
(
"configure_flow",
models.ForeignKey(
blank=True,
help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_flows.flow",
),
),
(
"user_verification",
models.TextField(
choices=[
("required", "Required"),
("preferred", "Preferred"),
("discouraged", "Discouraged"),
],
default="preferred",
),
),
(
"authenticator_attachment",
models.TextField(
choices=[("platform", "Platform"), ("cross-platform", "Cross Platform")],
default=None,
null=True,
),
),
(
"resident_key_requirement",
models.TextField(
choices=[
("discouraged", "Discouraged"),
("preferred", "Preferred"),
("required", "Required"),
],
default="preferred",
),
),
("friendly_name", models.TextField(null=True)),
(
"device_type_restrictions",
models.ManyToManyField(
blank=True, to="authentik_stages_authenticator_webauthn.webauthndevicetype"
),
),
],
options={
"verbose_name": "WebAuthn Authenticator Setup Stage",
"verbose_name_plural": "WebAuthn Authenticator Setup Stages",
},
bases=("authentik_flows.stage", models.Model),
),
migrations.CreateModel(
name="WebAuthnDevice",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.TextField(max_length=200)),
("credential_id", models.TextField(unique=True)),
("public_key", models.TextField()),
("sign_count", models.IntegerField(default=0)),
("rp_id", models.CharField(max_length=253)),
("created_on", models.DateTimeField(auto_now_add=True)),
("last_t", models.DateTimeField(default=django.utils.timezone.now)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
(
"confirmed",
models.BooleanField(default=True, help_text="Is this device ready for use?"),
),
(
"device_type",
models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_stages_authenticator_webauthn.webauthndevicetype",
),
),
("aaguid", models.TextField(default="00000000-0000-0000-0000-000000000000")),
],
options={
"verbose_name": "WebAuthn Device",
"verbose_name_plural": "WebAuthn Devices",
},
),
]

View File

@ -1,21 +0,0 @@
# Generated by Django 5.0.4 on 2024-04-18 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_stages_authenticator_webauthn",
"0010_webauthndevicetype_authenticatorwebauthnstage_and_more",
),
]
operations = [
migrations.AddField(
model_name="webauthndevice",
name="aaguid",
field=models.TextField(default="00000000-0000-0000-0000-000000000000"),
),
]

View File

@ -132,7 +132,6 @@ class WebAuthnDevice(SerializerModel, Device):
created_on = models.DateTimeField(auto_now_add=True)
last_t = models.DateTimeField(default=now)
aaguid = models.TextField(default=UNKNOWN_DEVICE_TYPE_AAGUID)
device_type = models.ForeignKey(
"WebAuthnDeviceType", on_delete=models.SET_DEFAULT, null=True, default=None
)

View File

@ -126,6 +126,10 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
if authenticator_attachment:
authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment))
attestation = AttestationConveyancePreference.DIRECT
if stage.device_type_restrictions.exists():
attestation = AttestationConveyancePreference.ENTERPRISE
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request),
rp_name=self.request.brand.branding_title,
@ -137,7 +141,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
user_verification=UserVerificationRequirement(str(stage.user_verification)),
authenticator_attachment=authenticator_attachment,
),
attestation=AttestationConveyancePreference.DIRECT,
attestation=attestation,
)
self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge
@ -176,7 +180,6 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
sign_count=webauthn_credential.sign_count,
rp_id=get_rp_id(self.request),
device_type=device_type,
aaguid=webauthn_credential.aaguid,
)
else:
return self.executor.stage_invalid("Device with Credential ID already exists.")

View File

@ -150,26 +150,22 @@ class PromptChallengeResponse(ChallengeResponse):
return attrs
def username_field_validator_factory() -> Callable[[PromptChallengeResponse, str], Any]:
def username_field_validator_factory() -> Callable[[PromptChallenge, str], Any]:
"""Return a `clean_` method for `field`. Clean method checks if username is taken already."""
def username_field_validator(self: PromptChallengeResponse, value: str) -> Any:
def username_field_validator(_: PromptChallenge, value: str) -> Any:
"""Check for duplicate usernames"""
pending_user = self.stage.get_pending_user()
query = User.objects.all()
if pending_user.pk:
query = query.exclude(username=pending_user.username)
if query.filter(username=value).exists():
if User.objects.filter(username=value).exists():
raise ValidationError("Username is already taken.")
return value
return username_field_validator
def password_single_validator_factory() -> Callable[[PromptChallengeResponse, str], Any]:
def password_single_validator_factory() -> Callable[[PromptChallenge, str], Any]:
"""Return a `clean_` method for `field`. Clean method checks if username is taken already."""
def password_single_clean(self: PromptChallengeResponse, value: str) -> Any:
def password_single_clean(self: PromptChallenge, value: str) -> Any:
"""Send password validation signals for e.g. LDAP Source"""
password_validate.send(sender=self, password=value, plan_context=self.plan.context)
return value

View File

@ -9,7 +9,6 @@ from django.utils.translation import gettext as _
from rest_framework.fields import BooleanField, CharField
from authentik.core.models import AuthenticatedSession, User
from authentik.events.middleware import audit_ignore
from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE
from authentik.flows.stage import ChallengeStageView
@ -96,14 +95,11 @@ class UserLoginStageView(ChallengeStageView):
self.logger.warning("User is not active, login will not work.")
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
# which we don't want to log as we already have a dedicated login event
with audit_ignore():
login(
self.request,
user,
backend=backend,
)
login(
self.request,
user,
backend=backend,
)
self.logger.debug(
"Logged in",
backend=backend,

View File

@ -16,7 +16,7 @@ entries:
placeholder: Username
placeholder_expression: false
required: true
type: username
type: text
field_key: username
label: Username
identifiers:

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.4.0 Blueprint schema",
"title": "authentik 2024.2.2 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -11,6 +11,7 @@ entries:
name: "authentik default LDAP Mapping: DN to User Path"
object_field: "path"
expression: |
dn = ldap.get("distinguishedName")
path_elements = []
for pair in dn.split(","):
attr, _, value = pair.partition("=")

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.2}
restart: unless-stopped
command: server
environment:
@ -53,7 +53,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.2}
restart: unless-stopped
command: worker
environment:

10
go.mod
View File

@ -1,13 +1,15 @@
module goauthentik.io
go 1.22.2
go 1.22
toolchain go1.22.0
require (
beryju.io/ldap v0.1.0
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/getsentry/sentry-go v0.27.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-ldap/ldap/v3 v3.4.7
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/strfmt v0.23.0
github.com/golang-jwt/jwt v3.2.2+incompatible
@ -28,7 +30,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024023.2
goauthentik.io/api/v3 v3.2024022.11
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.19.0
golang.org/x/sync v0.7.0
@ -73,7 +75,7 @@ require (
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect

11
go.sum
View File

@ -84,8 +84,8 @@ github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 h1:O6yi4xa9b2D
github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27/go.mod h1:AYvN8omj7nKLmbcXS2dyABYU6JB1Lz1bHmkkq1kf4I4=
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno=
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/go-ldap/ldap/v3 v3.4.7 h1:3Hbd7mIB1qjd3Ra59fI3JYea/t5kykFu2CVHBca9koE=
github.com/go-ldap/ldap/v3 v3.4.7/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -294,8 +294,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.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024023.2 h1:lSVaZAKTpsDhtw11wnkGjPalkDzv9H2VKEJllBi2aXs=
goauthentik.io/api/v3 v3.2024023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024022.11 h1:MlsaBwyMM9NtDvZcoaWvuNznPHXA0a5olnDLyr24REA=
goauthentik.io/api/v3 v3.2024022.11/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=
@ -373,9 +373,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View File

@ -25,14 +25,13 @@ type Config struct {
}
type RedisConfig struct {
Host string `yaml:"host" env:"HOST, overwrite"`
Port int `yaml:"port" env:"PORT, overwrite"`
DB int `yaml:"db" env:"DB, overwrite"`
Username string `yaml:"username" env:"USERNAME, overwrite"`
Password string `yaml:"password" env:"PASSWORD, overwrite"`
TLS bool `yaml:"tls" env:"TLS, overwrite"`
TLSReqs string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"`
TLSCaCert *string `yaml:"tls_ca_certs" env:"TLS_CA_CERT, overwrite"`
Host string `yaml:"host" env:"HOST, overwrite"`
Port int `yaml:"port" env:"PORT, overwrite"`
DB int `yaml:"db" env:"DB, overwrite"`
Username string `yaml:"username" env:"USERNAME, overwrite"`
Password string `yaml:"password" env:"PASSWORD, overwrite"`
TLS bool `yaml:"tls" env:"TLS, overwrite"`
TLSReqs string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"`
}
type ListenConfig struct {

View File

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

View File

@ -2,8 +2,6 @@ package application
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"math"
"net/http"
@ -21,7 +19,6 @@ import (
"goauthentik.io/internal/outpost/proxyv2/codecs"
"goauthentik.io/internal/outpost/proxyv2/constants"
"goauthentik.io/internal/outpost/proxyv2/redisstore"
"goauthentik.io/internal/utils"
)
const RedisKeyPrefix = "authentik_proxy_session_"
@ -34,40 +31,11 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
maxAge = int(*t) + 1
}
if a.isEmbedded {
var tls *tls.Config
if config.Get().Redis.TLS {
tls = utils.GetTLSConfig()
switch strings.ToLower(config.Get().Redis.TLSReqs) {
case "none":
case "false":
tls.InsecureSkipVerify = true
case "required":
break
}
ca := config.Get().Redis.TLSCaCert
if ca != nil {
// Get the SystemCertPool, continue with an empty pool on error
rootCAs, _ := x509.SystemCertPool()
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}
certs, err := os.ReadFile(*ca)
if err != nil {
a.log.WithError(err).Fatalf("Failed to append %s to RootCAs", *ca)
}
// Append our cert to the system pool
if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
a.log.Println("No certs appended, using system certs only")
}
tls.RootCAs = rootCAs
}
}
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port),
Username: config.Get().Redis.Username,
Password: config.Get().Redis.Password,
DB: config.Get().Redis.DB,
TLSConfig: tls,
Addr: fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port),
Username: config.Get().Redis.Username,
Password: config.Get().Redis.Password,
DB: config.Get().Redis.DB,
})
// New default RedisStore

View File

@ -54,7 +54,7 @@ function cleanup {
}
function prepare_debug {
VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction
poetry install --no-ansi --no-interaction
touch /unittest.xml
chown authentik:authentik /unittest.xml
}

View File

@ -9,7 +9,7 @@ from psycopg import OperationalError, connect
from redis import Redis
from redis.exceptions import RedisError
from authentik.lib.config import CONFIG, redis_url
from authentik.lib.config import CONFIG
def check_postgres():
@ -35,18 +35,24 @@ def check_postgres():
def check_redis():
url = redis_url(CONFIG.get("redis.db"))
REDIS_PROTOCOL_PREFIX = "redis://"
if CONFIG.get_bool("redis.tls", False):
REDIS_PROTOCOL_PREFIX = "rediss://"
REDIS_URL = (
f"{REDIS_PROTOCOL_PREFIX}"
f"{quote_plus(CONFIG.get('redis.username'))}:"
f"{quote_plus(CONFIG.get('redis.password'))}@"
f"{quote_plus(CONFIG.get('redis.host'))}:"
f"{CONFIG.get_int('redis.port')}/{CONFIG.get('redis.db')}"
)
while True:
try:
redis = Redis.from_url(url)
redis = Redis.from_url(REDIS_URL)
redis.ping()
break
except RedisError as exc:
sleep(1)
sanitized_url = url.replace(quote_plus(CONFIG.get("redis.password")), "******")
CONFIG.log(
"info", f"Redis Connection failed, retrying... ({exc})", redis_url=sanitized_url
)
CONFIG.log("info", f"Redis Connection failed, retrying... ({exc})", redis_url=REDIS_URL)
CONFIG.log("info", "Redis Connection successful")

View File

@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-16 00:07+0000\n"
"POT-Creation-Date: 2024-03-05 00:07+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2024\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -138,14 +138,6 @@ msgstr "Group"
msgid "Groups"
msgstr "Groupes"
#: authentik/core/models.py
msgid "Add user to group"
msgstr "Ajouter un utilisateur au groupe"
#: authentik/core/models.py
msgid "Remove user from group"
msgstr "Retirer l'utilisateur du groupe"
#: authentik/core/models.py
msgid "User's display name."
msgstr "Nom d'affichage de l'utilisateur"
@ -513,22 +505,6 @@ msgstr "Limite maximum de connection atteinte."
msgid "(You are already connected in another tab/window)"
msgstr "(Vous êtes déjà connecté dans un autre onglet/une autre fenêtre)"
#: authentik/enterprise/stages/source/models.py
msgid ""
"Amount of time a user can take to return from the source to continue the "
"flow (Format: hours=-1;minutes=-2;seconds=-3)"
msgstr ""
"Durée que l'utilisateur peut prendre pour revenir de la source pour "
"continuer le flux (Format: hours=-1;minutes=-2;seconds=-3)"
#: authentik/enterprise/stages/source/models.py
msgid "Source Stage"
msgstr "Étape Source"
#: authentik/enterprise/stages/source/models.py
msgid "Source Stages"
msgstr "Étapes Source"
#: authentik/events/api/tasks.py
#, python-brace-format
msgid "Successfully started task {name}."
@ -1968,12 +1944,6 @@ msgstr "Les objets appliqués à ce filtre seront des groupes."
msgid "Field which contains a unique Identifier."
msgstr "Champ qui contient un identifiant unique."
#: authentik/sources/ldap/models.py
msgid "Update internal authentik password when login succeeds with LDAP"
msgstr ""
"Mettre à jour le mot de passe interne à authentik lorsque la connexion avec "
"LDAP réussi"
#: authentik/sources/ldap/models.py
msgid ""
"When a user changes their password, sync it back to LDAP. This can only be "
@ -2312,14 +2282,6 @@ msgstr "Connexion de l'utilisateur à la source SAML"
msgid "User SAML Source Connections"
msgstr "Connexion de l'utilisateur aux sources SAML"
#: authentik/sources/scim/models.py
msgid "SCIM Source"
msgstr "Source SCIM"
#: authentik/sources/scim/models.py
msgid "SCIM Sources"
msgstr "Sources SCIM"
#: authentik/stages/authenticator_duo/models.py
msgid "Duo Authenticator Setup Stage"
msgstr "Étape de configuration du Duo Authenticator"
@ -2433,20 +2395,8 @@ msgid "TOTP Devices"
msgstr "Équipements TOTP"
#: authentik/stages/authenticator_validate/challenge.py
msgid ""
"Invalid Token. Please ensure the time on your device is accurate and try "
"again."
msgstr ""
"Jeton invalide. Merci de vous assurer que le temps défini sur votre appareil"
" est juste et de réessayer,"
#: authentik/stages/authenticator_validate/challenge.py
#: authentik/stages/authenticator_webauthn/stage.py
#, python-brace-format
msgid "Invalid device type. Contact your {brand} administrator for help."
msgstr ""
"Type d'appareil invalide. Merci de contacter l'administrateur de {brand} "
"pour de l'assistance."
msgid "Invalid Token"
msgstr "Jeton Invalide"
#: authentik/stages/authenticator_validate/models.py
msgid "Static"
@ -2502,10 +2452,6 @@ msgstr "Étape de validation de l'authentificateur"
msgid "Authenticator Validation Stages"
msgstr "Étapes de validation de l'authentificateur"
#: authentik/stages/authenticator_validate/stage.py
msgid "No (allowed) MFA authenticator configured."
msgstr "Pas d'authentificateur MFA (autorisé) configuré."
#: authentik/stages/authenticator_webauthn/models.py
msgid "WebAuthn Authenticator Setup Stage"
msgstr "Étape de validation de l'authentificateur WebAuthn"
@ -2522,14 +2468,6 @@ msgstr "Appareil WebAuthn"
msgid "WebAuthn Devices"
msgstr "Équipements WebAuthn"
#: authentik/stages/authenticator_webauthn/models.py
msgid "WebAuthn Device type"
msgstr "Type d'appareil WebAuthn"
#: authentik/stages/authenticator_webauthn/models.py
msgid "WebAuthn Device types"
msgstr "Types d'appareil WebAuthn"
#: authentik/stages/captcha/models.py
msgid "Public key, acquired your captcha Provider."
msgstr "Clé publique, acquise auprès de votre fournisseur captcha."
@ -3191,14 +3129,6 @@ msgstr ""
msgid "Globally enable/disable impersonation."
msgstr "Activer/désactiver l'appropriation utilisateur de manière globale."
#: authentik/tenants/models.py
msgid "Default token duration"
msgstr "Durée par défaut des jetons"
#: authentik/tenants/models.py
msgid "Default token length"
msgstr "Longueur par défaut des jetons"
#: authentik/tenants/models.py
msgid "Tenant"
msgstr "Tenant"

Binary file not shown.

226
poetry.lock generated
View File

@ -2,87 +2,87 @@
[[package]]
name = "aiohttp"
version = "3.9.4"
version = "3.9.2"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohttp-3.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:76d32588ef7e4a3f3adff1956a0ba96faabbdee58f2407c122dd45aa6e34f372"},
{file = "aiohttp-3.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:56181093c10dbc6ceb8a29dfeea1e815e1dfdc020169203d87fd8d37616f73f9"},
{file = "aiohttp-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7a5b676d3c65e88b3aca41816bf72831898fcd73f0cbb2680e9d88e819d1e4d"},
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1df528a85fb404899d4207a8d9934cfd6be626e30e5d3a5544a83dbae6d8a7e"},
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f595db1bceabd71c82e92df212dd9525a8a2c6947d39e3c994c4f27d2fe15b11"},
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c0b09d76e5a4caac3d27752027fbd43dc987b95f3748fad2b924a03fe8632ad"},
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689eb4356649ec9535b3686200b231876fb4cab4aca54e3bece71d37f50c1d13"},
{file = "aiohttp-3.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3666cf4182efdb44d73602379a66f5fdfd5da0db5e4520f0ac0dcca644a3497"},
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b65b0f8747b013570eea2f75726046fa54fa8e0c5db60f3b98dd5d161052004a"},
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1885d2470955f70dfdd33a02e1749613c5a9c5ab855f6db38e0b9389453dce7"},
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0593822dcdb9483d41f12041ff7c90d4d1033ec0e880bcfaf102919b715f47f1"},
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:47f6eb74e1ecb5e19a78f4a4228aa24df7fbab3b62d4a625d3f41194a08bd54f"},
{file = "aiohttp-3.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c8b04a3dbd54de6ccb7604242fe3ad67f2f3ca558f2d33fe19d4b08d90701a89"},
{file = "aiohttp-3.9.4-cp310-cp310-win32.whl", hash = "sha256:8a78dfb198a328bfb38e4308ca8167028920fb747ddcf086ce706fbdd23b2926"},
{file = "aiohttp-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:e78da6b55275987cbc89141a1d8e75f5070e577c482dd48bd9123a76a96f0bbb"},
{file = "aiohttp-3.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c111b3c69060d2bafc446917534150fd049e7aedd6cbf21ba526a5a97b4402a5"},
{file = "aiohttp-3.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efbdd51872cf170093998c87ccdf3cb5993add3559341a8e5708bcb311934c94"},
{file = "aiohttp-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bfdb41dc6e85d8535b00d73947548a748e9534e8e4fddd2638109ff3fb081df"},
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd9d334412961125e9f68d5b73c1d0ab9ea3f74a58a475e6b119f5293eee7ba"},
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35d78076736f4a668d57ade00c65d30a8ce28719d8a42471b2a06ccd1a2e3063"},
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:824dff4f9f4d0f59d0fa3577932ee9a20e09edec8a2f813e1d6b9f89ced8293f"},
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52b8b4e06fc15519019e128abedaeb56412b106ab88b3c452188ca47a25c4093"},
{file = "aiohttp-3.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eae569fb1e7559d4f3919965617bb39f9e753967fae55ce13454bec2d1c54f09"},
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:69b97aa5792428f321f72aeb2f118e56893371f27e0b7d05750bcad06fc42ca1"},
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d79aad0ad4b980663316f26d9a492e8fab2af77c69c0f33780a56843ad2f89e"},
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:d6577140cd7db19e430661e4b2653680194ea8c22c994bc65b7a19d8ec834403"},
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:9860d455847cd98eb67897f5957b7cd69fbcb436dd3f06099230f16a66e66f79"},
{file = "aiohttp-3.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69ff36d3f8f5652994e08bd22f093e11cfd0444cea310f92e01b45a4e46b624e"},
{file = "aiohttp-3.9.4-cp311-cp311-win32.whl", hash = "sha256:e27d3b5ed2c2013bce66ad67ee57cbf614288bda8cdf426c8d8fe548316f1b5f"},
{file = "aiohttp-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d6a67e26daa686a6fbdb600a9af8619c80a332556245fa8e86c747d226ab1a1e"},
{file = "aiohttp-3.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c5ff8ff44825736a4065d8544b43b43ee4c6dd1530f3a08e6c0578a813b0aa35"},
{file = "aiohttp-3.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d12a244627eba4e9dc52cbf924edef905ddd6cafc6513849b4876076a6f38b0e"},
{file = "aiohttp-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dcad56c8d8348e7e468899d2fb3b309b9bc59d94e6db08710555f7436156097f"},
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7e69a7fd4b5ce419238388e55abd220336bd32212c673ceabc57ccf3d05b55"},
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4870cb049f10d7680c239b55428916d84158798eb8f353e74fa2c98980dcc0b"},
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2feaf1b7031ede1bc0880cec4b0776fd347259a723d625357bb4b82f62687b"},
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:939393e8c3f0a5bcd33ef7ace67680c318dc2ae406f15e381c0054dd658397de"},
{file = "aiohttp-3.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d2334e387b2adcc944680bebcf412743f2caf4eeebd550f67249c1c3696be04"},
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e0198ea897680e480845ec0ffc5a14e8b694e25b3f104f63676d55bf76a82f1a"},
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e40d2cd22914d67c84824045861a5bb0fb46586b15dfe4f046c7495bf08306b2"},
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:aba80e77c227f4234aa34a5ff2b6ff30c5d6a827a91d22ff6b999de9175d71bd"},
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:fb68dc73bc8ac322d2e392a59a9e396c4f35cb6fdbdd749e139d1d6c985f2527"},
{file = "aiohttp-3.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f3460a92638dce7e47062cf088d6e7663adb135e936cb117be88d5e6c48c9d53"},
{file = "aiohttp-3.9.4-cp312-cp312-win32.whl", hash = "sha256:32dc814ddbb254f6170bca198fe307920f6c1308a5492f049f7f63554b88ef36"},
{file = "aiohttp-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:63f41a909d182d2b78fe3abef557fcc14da50c7852f70ae3be60e83ff64edba5"},
{file = "aiohttp-3.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c3770365675f6be220032f6609a8fbad994d6dcf3ef7dbcf295c7ee70884c9af"},
{file = "aiohttp-3.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:305edae1dea368ce09bcb858cf5a63a064f3bff4767dec6fa60a0cc0e805a1d3"},
{file = "aiohttp-3.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f121900131d116e4a93b55ab0d12ad72573f967b100e49086e496a9b24523ea"},
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b71e614c1ae35c3d62a293b19eface83d5e4d194e3eb2fabb10059d33e6e8cbf"},
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:419f009fa4cfde4d16a7fc070d64f36d70a8d35a90d71aa27670bba2be4fd039"},
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b39476ee69cfe64061fd77a73bf692c40021f8547cda617a3466530ef63f947"},
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b33f34c9c7decdb2ab99c74be6443942b730b56d9c5ee48fb7df2c86492f293c"},
{file = "aiohttp-3.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c78700130ce2dcebb1a8103202ae795be2fa8c9351d0dd22338fe3dac74847d9"},
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:268ba22d917655d1259af2d5659072b7dc11b4e1dc2cb9662fdd867d75afc6a4"},
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:17e7c051f53a0d2ebf33013a9cbf020bb4e098c4bc5bce6f7b0c962108d97eab"},
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7be99f4abb008cb38e144f85f515598f4c2c8932bf11b65add0ff59c9c876d99"},
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:d58a54d6ff08d2547656356eea8572b224e6f9bbc0cf55fa9966bcaac4ddfb10"},
{file = "aiohttp-3.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7673a76772bda15d0d10d1aa881b7911d0580c980dbd16e59d7ba1422b2d83cd"},
{file = "aiohttp-3.9.4-cp38-cp38-win32.whl", hash = "sha256:e4370dda04dc8951012f30e1ce7956a0a226ac0714a7b6c389fb2f43f22a250e"},
{file = "aiohttp-3.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:eb30c4510a691bb87081192a394fb661860e75ca3896c01c6d186febe7c88530"},
{file = "aiohttp-3.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:84e90494db7df3be5e056f91412f9fa9e611fbe8ce4aaef70647297f5943b276"},
{file = "aiohttp-3.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d4845f8501ab28ebfdbeab980a50a273b415cf69e96e4e674d43d86a464df9d"},
{file = "aiohttp-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69046cd9a2a17245c4ce3c1f1a4ff8c70c7701ef222fce3d1d8435f09042bba1"},
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b73a06bafc8dcc508420db43b4dd5850e41e69de99009d0351c4f3007960019"},
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:418bb0038dfafeac923823c2e63226179976c76f981a2aaad0ad5d51f2229bca"},
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a8f241456b6c2668374d5d28398f8e8cdae4cce568aaea54e0f39359cd928d"},
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935c369bf8acc2dc26f6eeb5222768aa7c62917c3554f7215f2ead7386b33748"},
{file = "aiohttp-3.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4e48c8752d14ecfb36d2ebb3d76d614320570e14de0a3aa7a726ff150a03c"},
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:916b0417aeddf2c8c61291238ce25286f391a6acb6f28005dd9ce282bd6311b6"},
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9b6787b6d0b3518b2ee4cbeadd24a507756ee703adbac1ab6dc7c4434b8c572a"},
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:221204dbda5ef350e8db6287937621cf75e85778b296c9c52260b522231940ed"},
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:10afd99b8251022ddf81eaed1d90f5a988e349ee7d779eb429fb07b670751e8c"},
{file = "aiohttp-3.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2506d9f7a9b91033201be9ffe7d89c6a54150b0578803cce5cb84a943d075bc3"},
{file = "aiohttp-3.9.4-cp39-cp39-win32.whl", hash = "sha256:e571fdd9efd65e86c6af2f332e0e95dad259bfe6beb5d15b3c3eca3a6eb5d87b"},
{file = "aiohttp-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:7d29dd5319d20aa3b7749719ac9685fbd926f71ac8c77b2477272725f882072d"},
{file = "aiohttp-3.9.4.tar.gz", hash = "sha256:6ff71ede6d9a5a58cfb7b6fffc83ab5d4a63138276c771ac91ceaaddf5459644"},
{file = "aiohttp-3.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:772fbe371788e61c58d6d3d904268e48a594ba866804d08c995ad71b144f94cb"},
{file = "aiohttp-3.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:edd4f1af2253f227ae311ab3d403d0c506c9b4410c7fc8d9573dec6d9740369f"},
{file = "aiohttp-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cfee9287778399fdef6f8a11c9e425e1cb13cc9920fd3a3df8f122500978292b"},
{file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc158466f6a980a6095ee55174d1de5730ad7dec251be655d9a6a9dd7ea1ff9"},
{file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54ec82f45d57c9a65a1ead3953b51c704f9587440e6682f689da97f3e8defa35"},
{file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abeb813a18eb387f0d835ef51f88568540ad0325807a77a6e501fed4610f864e"},
{file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc91d07280d7d169f3a0f9179d8babd0ee05c79d4d891447629ff0d7d8089ec2"},
{file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b65e861f4bebfb660f7f0f40fa3eb9f2ab9af10647d05dac824390e7af8f75b7"},
{file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04fd8ffd2be73d42bcf55fd78cde7958eeee6d4d8f73c3846b7cba491ecdb570"},
{file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3d8d962b439a859b3ded9a1e111a4615357b01620a546bc601f25b0211f2da81"},
{file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:8ceb658afd12b27552597cf9a65d9807d58aef45adbb58616cdd5ad4c258c39e"},
{file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0e4ee4df741670560b1bc393672035418bf9063718fee05e1796bf867e995fad"},
{file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2dec87a556f300d3211decf018bfd263424f0690fcca00de94a837949fbcea02"},
{file = "aiohttp-3.9.2-cp310-cp310-win32.whl", hash = "sha256:3e1a800f988ce7c4917f34096f81585a73dbf65b5c39618b37926b1238cf9bc4"},
{file = "aiohttp-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:ea510718a41b95c236c992b89fdfc3d04cc7ca60281f93aaada497c2b4e05c46"},
{file = "aiohttp-3.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6aaa6f99256dd1b5756a50891a20f0d252bd7bdb0854c5d440edab4495c9f973"},
{file = "aiohttp-3.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a27d8c70ad87bcfce2e97488652075a9bdd5b70093f50b10ae051dfe5e6baf37"},
{file = "aiohttp-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54287bcb74d21715ac8382e9de146d9442b5f133d9babb7e5d9e453faadd005e"},
{file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb3d05569aa83011fcb346b5266e00b04180105fcacc63743fc2e4a1862a891"},
{file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8534e7d69bb8e8d134fe2be9890d1b863518582f30c9874ed7ed12e48abe3c4"},
{file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd9d5b989d57b41e4ff56ab250c5ddf259f32db17159cce630fd543376bd96b"},
{file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa6904088e6642609981f919ba775838ebf7df7fe64998b1a954fb411ffb4663"},
{file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda42eb410be91b349fb4ee3a23a30ee301c391e503996a638d05659d76ea4c2"},
{file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:193cc1ccd69d819562cc7f345c815a6fc51d223b2ef22f23c1a0f67a88de9a72"},
{file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b9f1cb839b621f84a5b006848e336cf1496688059d2408e617af33e3470ba204"},
{file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:d22a0931848b8c7a023c695fa2057c6aaac19085f257d48baa24455e67df97ec"},
{file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4112d8ba61fbd0abd5d43a9cb312214565b446d926e282a6d7da3f5a5aa71d36"},
{file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c4ad4241b52bb2eb7a4d2bde060d31c2b255b8c6597dd8deac2f039168d14fd7"},
{file = "aiohttp-3.9.2-cp311-cp311-win32.whl", hash = "sha256:ee2661a3f5b529f4fc8a8ffee9f736ae054adfb353a0d2f78218be90617194b3"},
{file = "aiohttp-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:4deae2c165a5db1ed97df2868ef31ca3cc999988812e82386d22937d9d6fed52"},
{file = "aiohttp-3.9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6f4cdba12539215aaecf3c310ce9d067b0081a0795dd8a8805fdb67a65c0572a"},
{file = "aiohttp-3.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:84e843b33d5460a5c501c05539809ff3aee07436296ff9fbc4d327e32aa3a326"},
{file = "aiohttp-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8008d0f451d66140a5aa1c17e3eedc9d56e14207568cd42072c9d6b92bf19b52"},
{file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61c47ab8ef629793c086378b1df93d18438612d3ed60dca76c3422f4fbafa792"},
{file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc71f748e12284312f140eaa6599a520389273174b42c345d13c7e07792f4f57"},
{file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1c3a4d0ab2f75f22ec80bca62385db2e8810ee12efa8c9e92efea45c1849133"},
{file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a87aa0b13bbee025faa59fa58861303c2b064b9855d4c0e45ec70182bbeba1b"},
{file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2cc0d04688b9f4a7854c56c18aa7af9e5b0a87a28f934e2e596ba7e14783192"},
{file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1956e3ac376b1711c1533266dec4efd485f821d84c13ce1217d53e42c9e65f08"},
{file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:114da29f39eccd71b93a0fcacff178749a5c3559009b4a4498c2c173a6d74dff"},
{file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3f17999ae3927d8a9a823a1283b201344a0627272f92d4f3e3a4efe276972fe8"},
{file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:f31df6a32217a34ae2f813b152a6f348154f948c83213b690e59d9e84020925c"},
{file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7a75307ffe31329928a8d47eae0692192327c599113d41b278d4c12b54e1bd11"},
{file = "aiohttp-3.9.2-cp312-cp312-win32.whl", hash = "sha256:972b63d589ff8f305463593050a31b5ce91638918da38139b9d8deaba9e0fed7"},
{file = "aiohttp-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:200dc0246f0cb5405c80d18ac905c8350179c063ea1587580e3335bfc243ba6a"},
{file = "aiohttp-3.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:158564d0d1020e0d3fe919a81d97aadad35171e13e7b425b244ad4337fc6793a"},
{file = "aiohttp-3.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:da1346cd0ccb395f0ed16b113ebb626fa43b7b07fd7344fce33e7a4f04a8897a"},
{file = "aiohttp-3.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:eaa9256de26ea0334ffa25f1913ae15a51e35c529a1ed9af8e6286dd44312554"},
{file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1543e7fb00214fb4ccead42e6a7d86f3bb7c34751ec7c605cca7388e525fd0b4"},
{file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:186e94570433a004e05f31f632726ae0f2c9dee4762a9ce915769ce9c0a23d89"},
{file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d52d20832ac1560f4510d68e7ba8befbc801a2b77df12bd0cd2bcf3b049e52a4"},
{file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c45e4e815ac6af3b72ca2bde9b608d2571737bb1e2d42299fc1ffdf60f6f9a1"},
{file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa906b9bdfd4a7972dd0628dbbd6413d2062df5b431194486a78f0d2ae87bd55"},
{file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:68bbee9e17d66f17bb0010aa15a22c6eb28583edcc8b3212e2b8e3f77f3ebe2a"},
{file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4c189b64bd6d9a403a1a3f86a3ab3acbc3dc41a68f73a268a4f683f89a4dec1f"},
{file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:8a7876f794523123bca6d44bfecd89c9fec9ec897a25f3dd202ee7fc5c6525b7"},
{file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:d23fba734e3dd7b1d679b9473129cd52e4ec0e65a4512b488981a56420e708db"},
{file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b141753be581fab842a25cb319f79536d19c2a51995d7d8b29ee290169868eab"},
{file = "aiohttp-3.9.2-cp38-cp38-win32.whl", hash = "sha256:103daf41ff3b53ba6fa09ad410793e2e76c9d0269151812e5aba4b9dd674a7e8"},
{file = "aiohttp-3.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:328918a6c2835861ff7afa8c6d2c70c35fdaf996205d5932351bdd952f33fa2f"},
{file = "aiohttp-3.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5264d7327c9464786f74e4ec9342afbbb6ee70dfbb2ec9e3dfce7a54c8043aa3"},
{file = "aiohttp-3.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07205ae0015e05c78b3288c1517afa000823a678a41594b3fdc870878d645305"},
{file = "aiohttp-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0a1e638cffc3ec4d4784b8b4fd1cf28968febc4bd2718ffa25b99b96a741bd"},
{file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d43302a30ba1166325974858e6ef31727a23bdd12db40e725bec0f759abce505"},
{file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16a967685907003765855999af11a79b24e70b34dc710f77a38d21cd9fc4f5fe"},
{file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fa3ee92cd441d5c2d07ca88d7a9cef50f7ec975f0117cd0c62018022a184308"},
{file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b500c5ad9c07639d48615a770f49618130e61be36608fc9bc2d9bae31732b8f"},
{file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c07327b368745b1ce2393ae9e1aafed7073d9199e1dcba14e035cc646c7941bf"},
{file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc7d6502c23a0ec109687bf31909b3fb7b196faf198f8cff68c81b49eb316ea9"},
{file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:07be2be7071723c3509ab5c08108d3a74f2181d4964e869f2504aaab68f8d3e8"},
{file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:122468f6fee5fcbe67cb07014a08c195b3d4c41ff71e7b5160a7bcc41d585a5f"},
{file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:00a9abcea793c81e7f8778ca195a1714a64f6d7436c4c0bb168ad2a212627000"},
{file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a9825fdd64ecac5c670234d80bb52bdcaa4139d1f839165f548208b3779c6c6"},
{file = "aiohttp-3.9.2-cp39-cp39-win32.whl", hash = "sha256:5422cd9a4a00f24c7244e1b15aa9b87935c85fb6a00c8ac9b2527b38627a9211"},
{file = "aiohttp-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:7d579dcd5d82a86a46f725458418458fa43686f6a7b252f2966d359033ffc8ab"},
{file = "aiohttp-3.9.2.tar.gz", hash = "sha256:b0ad0a5e86ce73f5368a164c10ada10504bf91869c05ab75d982c6048217fbf7"},
]
[package.dependencies]
@ -547,13 +547,13 @@ test = ["coverage (>=7)", "hypothesis", "pytest"]
[[package]]
name = "celery"
version = "5.4.0"
version = "5.3.6"
description = "Distributed Task Queue."
optional = false
python-versions = ">=3.8"
files = [
{file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"},
{file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"},
{file = "celery-5.3.6-py3-none-any.whl", hash = "sha256:9da4ea0118d232ce97dff5ed4974587fb1c0ff5c10042eb15278487cdd27d1af"},
{file = "celery-5.3.6.tar.gz", hash = "sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9"},
]
[package.dependencies]
@ -569,7 +569,7 @@ vine = ">=5.1.0,<6.0"
[package.extras]
arangodb = ["pyArango (>=2.0.2)"]
auth = ["cryptography (==42.0.5)"]
auth = ["cryptography (==41.0.5)"]
azureblockblob = ["azure-storage-blob (>=12.15.0)"]
brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
cassandra = ["cassandra-driver (>=3.25.0,<4)"]
@ -579,23 +579,22 @@ couchbase = ["couchbase (>=3.0.0)"]
couchdb = ["pycouchdb (==1.14.2)"]
django = ["Django (>=2.2.28)"]
dynamodb = ["boto3 (>=1.26.143)"]
elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"]
elasticsearch = ["elastic-transport (<=8.10.0)", "elasticsearch (<=8.11.0)"]
eventlet = ["eventlet (>=0.32.0)"]
gcs = ["google-cloud-storage (>=2.10.0)"]
gevent = ["gevent (>=1.5.0)"]
librabbitmq = ["librabbitmq (>=2.0.0)"]
memcache = ["pylibmc (==1.6.3)"]
mongodb = ["pymongo[srv] (>=4.0.2)"]
msgpack = ["msgpack (==1.0.8)"]
pymemcache = ["python-memcached (>=1.61)"]
msgpack = ["msgpack (==1.0.7)"]
pymemcache = ["python-memcached (==1.59)"]
pyro = ["pyro4 (==4.82)"]
pytest = ["pytest-celery[all] (>=1.0.0)"]
pytest = ["pytest-celery (==0.0.0)"]
redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"]
s3 = ["boto3 (>=1.26.143)"]
slmq = ["softlayer-messaging (>=1.0.3)"]
solar = ["ephem (==4.1.5)"]
sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"]
sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.0)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"]
tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"]
yaml = ["PyYAML (>=3.10)"]
zookeeper = ["kazoo (>=1.3.1)"]
@ -1613,23 +1612,22 @@ requests = ["requests (>=2.20.0,<3.0.0.dev0)"]
[[package]]
name = "gunicorn"
version = "22.0.0"
version = "21.2.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.5"
files = [
{file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"},
{file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"},
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"},
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"},
]
[package.dependencies]
packaging = "*"
[package.extras]
eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
eventlet = ["eventlet (>=0.24.1)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"]
[[package]]
@ -3527,28 +3525,28 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.4.0"
version = "0.3.7"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:70b8c620cf2212744eabd6d69c4f839f2be0d8880d27beaeb0adb6aa0b316aa8"},
{file = "ruff-0.4.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa3e3ff53be05a8c5570c1585ea1e089f6b399ca99fcb78598d4a8234f248db"},
{file = "ruff-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5616cca501d1d16b932b7e607d7e1fd1b8c8c51d6ee484b7940fc1adc5bea541"},
{file = "ruff-0.4.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46eff08dd480b5d9b540846159fe134d70e3c45a3c913c600047cbf7f0e4e308"},
{file = "ruff-0.4.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d546f511431fff2b17adcf7110f3b2c2c0c8d33b0e10e5fd27fd340bc617efc"},
{file = "ruff-0.4.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7b6b6b38e216036284c5779b6aa14acbf5664e3b5872533219cf93daf40ddfb"},
{file = "ruff-0.4.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e1cf8b064bb2a6b4922af7274fe2dffcb552d96ba716b2fbe5e2c970ed7de18"},
{file = "ruff-0.4.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9911c9046b94253e1fa844c0192bb764b86866a881502dee324686474d498c17"},
{file = "ruff-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ca7a971c8f1a0b6f5ff4a819c0d1c2619536530bbd5a289af725d8b2ef1013d"},
{file = "ruff-0.4.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:752e0f77f421141dd470a0b1bed4fd8f763aebabe32c80ed3580f740ef4ba807"},
{file = "ruff-0.4.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:84f2a5dd8f33964d826c5377e094f7ce11e55e432cd42d3bf64efe4384224a03"},
{file = "ruff-0.4.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0b20e7db4a672495320a8a18149b7febf4e4f97509a4657367144569ce0915fd"},
{file = "ruff-0.4.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0b0eddd339e24dc4f7719b1cde4967f6b6bc0ad948cc183711ba8910f14aeafe"},
{file = "ruff-0.4.0-py3-none-win32.whl", hash = "sha256:e70befd488271a2c28c80bd427f73d8855dd222fc549fa1e9967d287c5cfe781"},
{file = "ruff-0.4.0-py3-none-win_amd64.whl", hash = "sha256:8584b9361900997ccf8d7aaa4dc4ab43e258a853ca7189d98ac929dc9ee50875"},
{file = "ruff-0.4.0-py3-none-win_arm64.whl", hash = "sha256:fea4ec813c965e40af29ee627a1579ee1d827d77e81d54b85bdd7b42d1540cdd"},
{file = "ruff-0.4.0.tar.gz", hash = "sha256:7457308d9ebf00d6a1c9a26aa755e477787a636c90b823f91cd7d4bea9e89260"},
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"},
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"},
{file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"},
{file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"},
{file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"},
{file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"},
]
[[package]]
@ -3965,13 +3963,13 @@ wsproto = ">=0.14"
[[package]]
name = "twilio"
version = "9.0.5"
version = "9.0.4"
description = "Twilio API client and TwiML generator"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "twilio-9.0.5-py2.py3-none-any.whl", hash = "sha256:5e09e910b9368f50f23cb3c3dd5ba77164d80a81e9d97db955cbac322deb2a4e"},
{file = "twilio-9.0.5.tar.gz", hash = "sha256:e9b5727943584d25d618fe502f0100fc5283215f31c863f80b5c64581b4702b0"},
{file = "twilio-9.0.4-py2.py3-none-any.whl", hash = "sha256:086abae1a575a0ee89a72c0792d814ee349fe55c8df76b563ecfc49463c3c533"},
{file = "twilio-9.0.4.tar.gz", hash = "sha256:d493d5bde6361bb713dffec00b9465ff84978b71334dd75002152e79604688ba"},
]
[package.dependencies]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.4.0"
version = "2024.2.2"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.4.0
version: 2024.2.2
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -44714,26 +44714,6 @@ components:
- num_pk
- parent_name
- pk
UserGroupRequest:
type: object
description: Simplified Group Serializer for user's groups
properties:
name:
type: string
minLength: 1
maxLength: 80
is_superuser:
type: boolean
description: Users added to this group will be superusers.
parent:
type: string
format: uuid
nullable: true
attributes:
type: object
additionalProperties: {}
required:
- name
UserLoginChallenge:
type: object
description: Empty challenge
@ -45420,11 +45400,7 @@ components:
- $ref: '#/components/schemas/WebAuthnDeviceType'
readOnly: true
nullable: true
aaguid:
type: string
readOnly: true
required:
- aaguid
- created_on
- device_type
- name

View File

@ -4,6 +4,7 @@ services:
postgresql:
container_name: postgres
image: docker.io/library/postgres:16
command: "-c max_connections=500"
volumes:
- db-data:/var/lib/postgresql/data
environment:

View File

View File

@ -0,0 +1,30 @@
---
services:
prometheus:
image: quay.io/prometheus/prometheus:latest
restart: unless-stopped
command:
- --enable-feature=native-histograms
- --web.enable-remote-write-receiver
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --web.console.libraries=/usr/share/prometheus/console_libraries
- --web.console.templates=/usr/share/prometheus/consoles
ports:
- 127.0.0.1:9090:9090
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus:/prometheus
user: root
grafana:
image: grafana/grafana:latest
restart: unless-stopped
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
ports:
- 127.0.0.1:3000:3000
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro

View File

@ -0,0 +1,79 @@
import exec from "k6/execution";
import http from "k6/http";
import { check } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
discardResponseBodies: true,
scenarios: Object.fromEntries(
[
// Number of events, page size
[1000, 100],
[10000, 20],
[10000, 100],
[100000, 100],
[1000000, 100],
].map((obj, i) => [
`${obj[0]}_${obj[1]}`,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
EVENT_COUNT: `${obj[0]}`,
PAGE_SIZE: `${obj[1]}`,
},
tags: {
testid: `event_list_${obj[0]}_${obj[1]}`,
event_count: `${obj[0]}`,
page_size: `${obj[1]}`,
},
},
]),
),
};
export default function () {
const event_count = Number(__ENV.EVENT_COUNT);
const domain = `event-list-${event_count}.${host}:9000`;
const page_size = Number(__ENV.PAGE_SIZE);
const pages = Math.round(event_count / page_size);
const params = {
headers: {
Authorization: "Bearer akadmin",
"Content-Type": "application/json",
Accept: "*/*",
},
};
if (pages <= 10) {
for (let page = 1; page <= pages; page++) {
let res = http.get(
http.url`http://${domain}/api/v3/events/events/?page=${page}&page_size=${page_size}`,
params,
);
check(res, {
"status is 200": (res) => res.status === 200,
});
}
} else {
let requests = [];
for (let page = 1; page <= pages; page++) {
requests.push([
"GET",
http.url`http://${domain}/api/v3/events/events/?page=${page}&page_size=${page_size}`,
null,
params,
]);
}
const responses = http.batch(requests);
for (let page = 1; page <= pages; page++) {
check(responses[page - 1], {
"status is 200": (res) => res.status === 200,
});
}
}
}

345
tests/benchmark/fixtures.py Executable file
View File

@ -0,0 +1,345 @@
#!/usr/bin/env python3
import random
import sys
from collections.abc import Iterable
from multiprocessing import Process
from os import environ
from uuid import uuid4
import django
environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
environ.setdefault("AUTHENTIK_BOOTSTRAP_PASSWORD", "akadmin")
environ.setdefault("AUTHENTIK_BOOTSTRAP_TOKEN", "akadmin")
environ.setdefault("AUTHENTIK_BOOTSTRAP_EMAIL", "akadmin@authentik.test")
django.setup()
from django.conf import settings
from authentik.core.models import Application, Group, User
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.stages.authenticator_static.models import StaticToken
from authentik.tenants.models import Domain, Tenant
settings.CELERY["task_always_eager"] = True
host = environ.get("BENCH_HOST", "localhost")
class TestSuite:
TEST_NAME: str
TEST_CASES: Iterable[Iterable[int | str | bool]]
@classmethod
def get_testcases(cls):
return [cls(params) for params in cls.TEST_CASES]
def __init__(self, params: Iterable[int | str | bool]):
self.params = params
def __str__(self):
return (
"-".join([self.TEST_NAME] + [str(param) for param in self.params])
.replace("_", "-")
.lower()
)
@property
def schema_name(self):
return f"t_{str(self).replace('-', '_')}"
@property
def domain_name(self):
return f"{str(self)}.{host}"
def create(self):
created = False
t = Tenant.objects.filter(schema_name=self.schema_name).first()
if not t:
created = True
t = Tenant.objects.create(schema_name=self.schema_name, name=uuid4())
Domain.objects.get_or_create(tenant=t, domain=self.domain_name)
if created:
with t:
self.create_data(*self.params)
def create_data(self):
raise NotImplementedError
def delete(self):
Tenant.objects.filter(schema_name=self.schema_name).delete()
class UserList(TestSuite):
TEST_NAME = "user-list"
TEST_CASES = [
(1000, 0, 0),
(10000, 0, 0),
(1000, 3, 0),
(10000, 3, 0),
(1000, 20, 0),
(10000, 20, 0),
(1000, 20, 3),
(10000, 20, 3),
]
def create_data(self, user_count: int, groups_per_user: int, parents_per_group: int):
Group.objects.bulk_create([Group(name=uuid4()) for _ in range(groups_per_user * 5)])
for group in Group.objects.exclude(name="authentik Admins"):
for _ in range(parents_per_group):
new_group = Group.objects.create(name=uuid4())
group.parent = new_group
group.save()
group = new_group
User.objects.bulk_create(
[
User(
username=uuid4(),
name=uuid4(),
)
for _ in range(user_count)
]
)
if groups_per_user:
for user in User.objects.exclude_anonymous().exclude(username="akadmin"):
user.ak_groups.set(
Group.objects.exclude(name="authentik Admins").order_by("?")[:groups_per_user]
)
class GroupList(TestSuite):
TEST_NAME = "group-list"
TEST_CASES = [
(1000, 0, False),
(10000, 0, False),
(1000, 1000, False),
(1000, 10000, False),
(1000, 0, True),
(10000, 0, True),
]
def create_data(self, group_count, users_per_group, with_parent):
User.objects.bulk_create(
[
User(
username=uuid4(),
name=uuid4(),
)
for _ in range(users_per_group * 5)
]
)
if with_parent:
parents = Group.objects.bulk_create([Group(name=uuid4()) for _ in range(group_count)])
groups = Group.objects.bulk_create(
[
Group(name=uuid4(), parent=(parents[i] if with_parent else None))
for i in range(group_count)
]
)
if users_per_group:
for group in groups:
group.users.set(
User.objects.exclude_anonymous()
.exclude(username="akadmin")
.order_by("?")[:users_per_group]
)
class Login(TestSuite):
TEST_NAME = "login"
TEST_CASES = [
("no-mfa",),
("with-mfa",),
]
def create_data(self, mfa: str):
user = User(username="test", name=uuid4())
user.set_password("verySecurePassword")
user.save()
if mfa == "with-mfa":
device = user.staticdevice_set.create()
# Multiple token with same token for all the iterations in the test
device.token_set.bulk_create(
[StaticToken(device=device, token=f"staticToken") for _ in range(1_000_000)]
)
class ProviderOauth2(TestSuite):
TEST_NAME = "provider-oauth2"
TEST_CASES = [
(2, 50, 2),
(0, 0, 0),
(10, 0, 0),
(100, 0, 0),
(0, 10, 0),
(0, 100, 0),
(0, 0, 10),
(0, 0, 100),
(10, 10, 10),
(100, 100, 100),
]
def create_data(
self, user_policies_count: int, group_policies_count: int, expression_policies_count: int
):
user = User(username="test", name=uuid4())
user.set_password("verySecurePassword")
user.save()
provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
),
signing_key=CertificateKeyPair.objects.get(name="authentik Self-signed Certificate"),
redirect_uris="http://test.localhost",
client_id="123456",
client_secret="123456",
)
application = Application.objects.create(slug="test", name="test", provider=provider)
User.objects.bulk_create(
[
User(
username=uuid4(),
name=uuid4(),
)
for _ in range(user_policies_count)
]
)
PolicyBinding.objects.bulk_create(
[
PolicyBinding(
user=user,
target=application,
order=random.randint(1, 1_000_000),
)
for user in User.objects.exclude(username="akadmin").exclude_anonymous()
]
)
Group.objects.bulk_create([Group(name=uuid4()) for _ in range(group_policies_count)])
PolicyBinding.objects.bulk_create(
[
PolicyBinding(
group=group,
target=application,
order=random.randint(1, 1_000_000),
)
for group in Group.objects.exclude(name="authentik Admins")
]
)
user.ak_groups.set(Group.objects.exclude(name="authentik Admins").order_by("?")[:1])
[
ExpressionPolicy(
name=f"test-{uuid4()}",
expression="return True",
).save()
for _ in range(expression_policies_count)
]
PolicyBinding.objects.bulk_create(
[
PolicyBinding(
policy=policy,
target=application,
order=random.randint(1, 1_000_000),
)
for policy in ExpressionPolicy.objects.filter(name__startswith="test-")
]
)
class EventList(TestSuite):
TEST_NAME = "event-list"
TEST_CASES = [
(1_000,),
(10_000,),
(100_000,),
(1_000_000,),
]
def create_data(self, event_count: int):
for _ in range(event_count // 1000):
Event.objects.bulk_create(
[
Event(
user={
"pk": str(uuid4()),
"name": str(uuid4()),
"username": str(uuid4()),
"email": f"{uuid4()}@example.org",
},
action="custom_benchmark",
app="tests_benchmarks",
context={
str(uuid4()): str(uuid4()),
str(uuid4()): str(uuid4()),
str(uuid4()): str(uuid4()),
str(uuid4()): str(uuid4()),
str(uuid4()): str(uuid4()),
},
client_ip="192.0.2.42",
)
for _ in range(1000)
]
)
class UserGroupCreate(TestSuite):
TEST_NAME = "user-group-create"
TEST_CASES = [
(),
]
def create_data(self):
pass
def main(action: str, selected_suite: str | None = None):
testsuites = TestSuite.__subclasses__()
testcases = []
for testsuite in testsuites:
testcases += testsuite.get_testcases()
match action:
case "create":
to_create = []
for testcase in testcases:
if selected_suite and testcase.TEST_NAME != selected_suite:
continue
testcase.create()
# to_create.append(testcase)
# processes = [Process(target=testcase.create) for testcase in to_create]
# for p in processes:
# p.start()
# for p in processes:
# p.join()
case "list":
print(*[testsuite.TEST_NAME for testsuite in testsuites], sep="\n")
case "delete":
for testcase in testcases:
if selected_suite and testcase.TEST_NAME != selected_suite:
continue
testcase.delete()
case _:
print("Unknown action. Should be create, list or delete")
exit(1)
if __name__ == "__main__":
if len(sys.argv) < 2:
action = "create"
else:
action = sys.argv[1]
if len(sys.argv) < 3:
testsuite = None
else:
testsuite = sys.argv[2]
main(action, testsuite)

View File

@ -0,0 +1,8 @@
---
apiVersion: 1
providers:
- name: default
folder: k6
type: file
options:
path: /var/lib/grafana/dashboards

View File

@ -0,0 +1,11 @@
---
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
uid: prometheus
url: http://prometheus:9090
jsonData:
timeInterval: 1s

View File

@ -0,0 +1,93 @@
import exec from "k6/execution";
import http from "k6/http";
import { check } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
discardResponseBodies: true,
scenarios: Object.fromEntries(
[
// Number of groups, number of users per group, with parent, page size, include users
[1000, 0, false, 20, false],
[10000, 0, false, 20, false],
[1000, 0, false, 100, false],
[10000, 0, false, 100, false],
[1000, 1000, false, 100, false],
[1000, 10000, false, 100, false],
[1000, 1000, false, 100, true],
[1000, 10000, false, 100, true],
[1000, 0, true, 100, false],
[10000, 0, true, 100, false],
].map((obj, i) => [
`${obj[0]}_${obj[1]}_${obj[2] ? "with_parents" : "without_parents"}_${obj[3]}_${obj[4] ? "with_users" : "without_users"}`,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
GROUP_COUNT: `${obj[0]}`,
USERS_PER_GROUP: `${obj[1]}`,
WITH_PARENTS: `${obj[2]}`,
PAGE_SIZE: `${obj[3]}`,
WITH_USERS: `${obj[4] ? "true" : "false"}`,
},
tags: {
testid: `group_list_${obj[0]}_${obj[1]}_${obj[2] ? "with_parents" : "without_parents"}_${obj[3]}_${obj[4] ? "with_users" : "without_users"}`,
group_count: `${obj[0]}`,
users_per_group: `${obj[1]}`,
with_parents: `${obj[2]}`,
page_size: `${obj[3]}`,
with_users: `${obj[4] ? "true" : "false"}`,
},
},
]),
),
};
export default function () {
const group_count = Number(__ENV.GROUP_COUNT);
const users_per_group = Number(__ENV.USERS_PER_GROUP);
const with_parents = __ENV.WITH_PARENTS;
const with_users = __ENV.WITH_USERS;
const domain = `group-list-${group_count}-${users_per_group}-${with_parents}.${host}:9000`;
const page_size = Number(__ENV.PAGE_SIZE);
const pages = Math.round(group_count / page_size);
const params = {
headers: {
Authorization: "Bearer akadmin",
"Content-Type": "application/json",
Accept: "*/*",
},
};
if (pages <= 10) {
for (let page = 1; page <= pages; page++) {
let res = http.get(
http.url`http://${domain}/api/v3/core/groups/?page=${page}&page_size=${page_size}&include_users=${with_users}`,
params,
);
check(res, {
"status is 200": (res) => res.status === 200,
});
}
} else {
let requests = [];
for (let page = 1; page <= pages; page++) {
requests.push([
"GET",
http.url`http://${domain}/api/v3/core/groups/?page=${page}&page_size=${page_size}&include_users=${with_users}`,
null,
params,
]);
}
const responses = http.batch(requests);
for (let page = 1; page <= pages; page++) {
check(responses[page - 1], {
"status is 200": (res) => res.status === 200,
});
}
}
}

78
tests/benchmark/login.js Normal file
View File

@ -0,0 +1,78 @@
import http from "k6/http";
import { check, fail } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
scenarios: Object.fromEntries(
["no-mfa", "with-mfa"].map((obj, i) => [
obj,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
DOMAIN: `login-${obj}`,
},
tags: {
testid: `login-${obj}`,
},
},
]),
),
};
export default function () {
const domain = __ENV.DOMAIN;
const url = http.url`http://${domain}.${host}:9000/api/v3/flows/executor/default-authentication-flow/`;
const cookieJar = new http.CookieJar();
const params = {
jar: cookieJar,
headers: {
"Content-Type": "application/json",
Accept: "*/*",
},
};
let res = http.get(url, params);
let i = 0;
while (true) {
if (i > 10) {
fail("Test made more than 10 queries.");
break;
}
check(res, {
"status is 200": (res) => res.status === 200,
});
if (res.status !== 200) {
fail("Endpoint did not return 200.");
break;
}
const component = res.json()["component"];
let payload = {};
if (component === "ak-stage-identification") {
payload = {
uid_field: "test",
};
} else if (component === "ak-stage-password") {
payload = {
password: "verySecurePassword",
};
} else if (component === "ak-stage-authenticator-validate") {
payload = {
code: "staticToken",
};
} else if (component === "xak-flow-redirect") {
break;
} else {
console.log(`Unknown component type: ${component}`);
break;
}
payload["component"] = component;
res = http.post(url, JSON.stringify(payload), params);
i++;
}
}

View File

@ -0,0 +1,13 @@
---
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: benchmarks
prometheus: benchmarks
prometheus_replica: "0"
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]

View File

@ -0,0 +1,179 @@
import crypto from "k6/crypto";
import exec from "k6/execution";
import http from "k6/http";
import { check, fail } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
const testcases = [
[2, 50, 2],
[0, 0, 0],
[10, 0, 0],
[100, 0, 0],
[0, 10, 0],
[0, 100, 0],
[0, 0, 10],
[0, 0, 100],
[10, 10, 10],
[100, 100, 100],
];
export const options = {
setupTimeout: "10m",
scenarios: Object.fromEntries(
testcases.map((obj, i) => [
`${obj[0]}_${obj[1]}_${obj[2]}`,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
USER_POLICIES_COUNT: `${obj[0]}`,
GROUP_POLICIES_COUNT: `${obj[1]}`,
EXPRESSION_POLICIES_COUNT: `${obj[2]}`,
},
tags: {
testid: `provider-oauth2-${obj[0]}_${obj[1]}_${obj[2]}`,
user_policies_count: `${obj[0]}`,
group_policies_count: `${obj[1]}`,
expression_policies_count: `${obj[2]}`,
},
},
]),
),
};
export function setup() {
let cookies = {};
for (let vu = 0; vu < VUs; vu++) {
cookies[vu] = {};
for (const testcase of testcases) {
const user_policies_count = testcase[0];
const group_policies_count = testcase[1];
const expression_policies_count = testcase[2];
const domain = `provider-oauth2-${user_policies_count}-${group_policies_count}-${expression_policies_count}.${host}:9000`;
const url = http.url`http://${domain}/api/v3/flows/executor/default-authentication-flow/`;
const params = {
headers: {
"Content-Type": "application/json",
Accept: "*/*",
},
};
http.cookieJar().clear(`http://${domain}`);
let res = http.get(url, params);
let i = 0;
while (true) {
if (i > 10) {
fail("Test made more than 10 queries.");
break;
}
check(res, {
"status is 200": (res) => res.status === 200,
});
if (res.status !== 200) {
fail("Endpoint did not return 200.");
break;
}
const component = res.json()["component"];
let payload = {};
if (component === "ak-stage-identification") {
payload = {
uid_field: "test",
};
} else if (component === "ak-stage-password") {
payload = {
password: "verySecurePassword",
};
} else if (component === "xak-flow-redirect") {
break;
} else {
fail(`Unknown component type: ${component}`);
break;
}
payload["component"] = component;
res = http.post(url, JSON.stringify(payload), params);
i++;
}
cookies[vu][domain] = http
.cookieJar()
.cookiesForURL(`http://${domain}`);
}
}
return { cookies };
}
export default function (data) {
// Restore cookies
let jar = http.cookieJar();
const vu = exec.vu.idInTest % VUs;
Object.keys(data.cookies[vu]).forEach((domain) => {
Object.keys(data.cookies[vu][domain]).forEach((key) => {
jar.set(`http://${domain}`, key, data.cookies[vu][domain][key][0]);
});
});
const user_policies_count = Number(__ENV.USER_POLICIES_COUNT);
const group_policies_count = Number(__ENV.GROUP_POLICIES_COUNT);
const expression_policies_count = Number(__ENV.EXPRESSION_POLICIES_COUNT);
const domain = `provider-oauth2-${user_policies_count}-${group_policies_count}-${expression_policies_count}.${host}:9000`;
const params = {
headers: {
"Content-Type": "application/json",
Accept: "*/*",
},
};
const random = (length = 32) => {
let chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let str = "";
for (let i = 0; i < length; i++) {
str += chars.charAt(Math.floor(Math.random() * chars.length));
}
return str;
};
const state = random(32);
const nonce = random(32);
const code_verifier = random(64);
const code_challenge = crypto.sha256(code_verifier, "base64");
const urlParams = {
response_type: "code",
scope: "openid profile email",
client_id: "123456",
redirect_uri: "http://test.localhost",
state: state,
nonce: nonce,
code_challenge: code_challenge,
code_challenge_method: "S256",
};
let url = http.url`http://${domain}/application/o/authorize/?${Object.entries(
urlParams,
)
.map((kv) => kv.map(encodeURIComponent).join("="))
.join("&")}`;
let res = http.get(url, params);
check(res, {
"status is 200": (res) => res.status === 200,
});
if (res.status !== 200) {
fail("Endpoint did not return 200.");
return;
}
url = http.url`http://${domain}/api/v3/flows/executor/default-provider-authorization-implicit-consent/`;
res = http.get(url, params);
check(res, {
"status is 200": (res) => res.status === 200,
"last redirect is present": (res) => res.json()["type"] === "redirect",
});
if (res.status !== 200) {
fail("Endpoint did not return 200.");
return;
}
}

32
tests/benchmark/run.sh Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
function _k6 {
local filename="${1}"
K6_PROMETHEUS_RW_SERVER_URL=${PROMETHEUS_REMOTE_WRITE_ENDPOINT:-http://localhost:9090/api/v1/write} \
K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true \
K6_PROMETHEUS_RW_PUSH_INTERVAL=1s \
k6 run \
--out experimental-prometheus-rw \
--out "json=${filename%.*}.json" \
"${@}"
}
filename=""
if [ "${#}" -ge 1 ]; then
filename="${1:-}"
shift
fi
if [ -f "${filename}" ]; then
_k6 "${filename}" "${@}"
else
find "${BASE_DIR}" -name '*.js' | while read -r f; do
_k6 "${f}" "${@}"
done
fi

View File

@ -0,0 +1,68 @@
import exec from "k6/execution";
import http from "k6/http";
import { check } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
vus: VUs,
duration: "150s",
tags: {
testid: `user-group-create`,
},
};
export default function () {
const domain = `user-group-create.${host}:9000`;
const params = {
headers: {
Authorization: "Bearer akadmin",
"Content-Type": "application/json",
Accept: "*/*",
},
};
const random = (length = 32) => {
let chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let str = "";
for (let i = 0; i < length; i++) {
str += chars.charAt(Math.floor(Math.random() * chars.length));
}
return str;
};
let user_res = http.post(
http.url`http://${domain}/api/v3/core/users/`,
JSON.stringify({
username: random(16),
name: random(16),
}),
params,
);
check(user_res, {
"user status is 201": (res) => res.status === 201,
});
let group_res = http.post(
http.url`http://${domain}/api/v3/core/groups/`,
JSON.stringify({
name: random(16),
}),
params,
);
check(group_res, {
"group status is 201": (res) => res.status === 201,
});
let user_group_res = http.post(
http.url`http://${domain}/api/v3/core/groups/${group_res.json()["pk"]}/add_user/`,
JSON.stringify({
pk: user_res.json()["pk"],
}),
params,
);
check(user_group_res, {
"user group status is 204": (res) => res.status === 204,
});
}

View File

@ -0,0 +1,99 @@
import exec from "k6/execution";
import http from "k6/http";
import { check } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
discardResponseBodies: true,
scenarios: Object.fromEntries(
[
// Number of users, number of groups per user, number of parents per group, page size, with groups
[1000, 0, 0, 20, true],
[10000, 0, 0, 20, true],
[1000, 0, 0, 20, false],
[10000, 0, 0, 20, false],
[1000, 0, 0, 100, true],
[10000, 0, 0, 100, true],
[1000, 3, 0, 20, true],
[10000, 3, 0, 20, true],
[1000, 20, 0, 20, true],
[10000, 20, 0, 20, true],
[1000, 20, 3, 20, true],
[10000, 20, 3, 20, true],
[1000, 20, 0, 20, false],
[10000, 20, 0, 20, false],
[1000, 20, 3, 20, false],
[10000, 20, 3, 20, false],
].map((obj, i) => [
`${obj[0]}_${obj[1]}_${obj[2]}_${obj[3]}_${obj[4] ? "with_groups" : "without_groups"}`,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
USER_COUNT: `${obj[0]}`,
GROUPS_PER_USER: `${obj[1]}`,
PARENTS_PER_GROUP: `${obj[2]}`,
PAGE_SIZE: `${obj[3]}`,
WITH_GROUPS: `${obj[4] ? "true" : "false"}`,
},
tags: {
testid: `user_list_${obj[0]}_${obj[1]}_${obj[2]}_${obj[3]}_${obj[4] ? "with_groups" : "without_groups"}`,
user_count: `${obj[0]}`,
groups_per_user: `${obj[1]}`,
parents_per_group: `${obj[2]}`,
page_size: `${obj[3]}`,
with_groups: `${obj[4] ? "true" : "false"}`,
},
},
]),
),
};
export default function () {
const user_count = Number(__ENV.USER_COUNT);
const groups_per_user = Number(__ENV.GROUPS_PER_USER);
const parents_per_group = Number(__ENV.PARENTS_PER_GROUP);
const with_groups = __ENV.WITH_GROUPS;
const domain = `user-list-${user_count}-${groups_per_user}-${parents_per_group}.${host}:9000`;
const page_size = Number(__ENV.PAGE_SIZE);
const pages = Math.round(user_count / page_size);
const params = {
headers: {
Authorization: "Bearer akadmin",
"Content-Type": "application/json",
Accept: "*/*",
},
};
if (pages <= 10) {
for (let page = 1; page <= pages; page++) {
let res = http.get(
http.url`http://${domain}/api/v3/core/users/?page=${page}&page_size=${page_size}&include_groups=${with_groups}`,
params,
);
check(res, {
"status is 100": (res) => res.status === 200,
});
}
} else {
let requests = [];
for (let page = 1; page <= pages; page++) {
requests.push([
"GET",
http.url`http://${domain}/api/v3/core/users/?page=${page}&page_size=${page_size}&include_groups=${with_groups}`,
null,
params,
]);
}
const responses = http.batch(requests);
for (let page = 1; page <= pages; page++) {
check(responses[page - 1], {
"status is 200": (res) => res.status === 200,
});
}
}
}

View File

@ -6,6 +6,7 @@ from typing import Any
from docker.types import Healthcheck
from authentik.core.models import Token, TokenIntents, User
from authentik.lib.generators import generate_id
from authentik.lib.utils.http import get_http_session
from authentik.sources.scim.models import SCIMSource
@ -39,9 +40,18 @@ class TestSourceSCIM(SeleniumTestCase):
@retry()
def test_scim_conformance(self):
user = User.objects.create(
username=generate_id(),
)
token = Token.objects.create(
user=user,
intent=TokenIntents.INTENT_API,
expiring=False,
)
source = SCIMSource.objects.create(
name=generate_id(),
slug=generate_id(),
token=token,
)
session = get_http_session()
test_launch = session.post(
@ -49,7 +59,7 @@ class TestSourceSCIM(SeleniumTestCase):
data={
"endPoint": self.live_server_url + f"/source/scim/{source.slug}/v2",
"username": "foo",
"password": source.token.key,
"password": token.key,
"jwtToken": None,
"usersCheck": 1,
"groupsCheck": 1,

View File

@ -6,7 +6,7 @@
"": {
"name": "@goauthentik/web-tests",
"dependencies": {
"chromedriver": "^123.0.4"
"chromedriver": "^123.0.3"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
@ -2084,9 +2084,9 @@
}
},
"node_modules/chromedriver": {
"version": "123.0.4",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.4.tgz",
"integrity": "sha512-3Yi7y7q35kkSAOTbRisiww/SL2w+DqafDPAaUShpSuLMmPaOvHQR0i3bm2/33QBiQ8fUb1J/MzppzVL6IDqvhA==",
"version": "123.0.3",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.3.tgz",
"integrity": "sha512-35IeTqDLcVR0htF9nD/Lh+g24EG088WHVKXBXiFyWq+2lelnoM0B3tKTBiUEjLng0GnELI4QyQPFK7i97Fz1fQ==",
"hasInstallScript": true,
"dependencies": {
"@testim/chrome-version": "^1.1.4",

View File

@ -32,6 +32,6 @@
"node": ">=20"
},
"dependencies": {
"chromedriver": "^123.0.4"
"chromedriver": "^123.0.3"
}
}

5015
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,22 +38,22 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.2.3-1713441634",
"@goauthentik/api": "^2024.2.2-1713289394",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.1",
"@lit/localize": "^0.12.1",
"@lit/reactive-element": "^2.0.4",
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^3.0.1",
"@patternfly/elements": "^3.0.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.111.0",
"@sentry/browser": "^7.110.1",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.4.2",
"chartjs-adapter-moment": "^1.0.1",
"codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.37.0",
"core-js": "^3.36.1",
"country-flag-icons": "^1.5.11",
"fuse.js": "^7.0.0",
"guacamole-common-js": "^1.5.0",
@ -81,13 +81,13 @@
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.5",
"@spotlightjs/spotlight": "^1.2.17",
"@storybook/addon-essentials": "^8.0.8",
"@storybook/addon-links": "^8.0.8",
"@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-links": "^7.6.17",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.0.8",
"@storybook/manager-api": "^8.0.8",
"@storybook/web-components": "^8.0.8",
"@storybook/web-components-vite": "^8.0.8",
"@storybook/blocks": "^7.6.4",
"@storybook/manager-api": "^7.6.17",
"@storybook/web-components": "^7.6.17",
"@storybook/web-components-vite": "^7.6.17",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15",
@ -117,8 +117,8 @@
"react-dom": "^18.2.0",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.0.8",
"storybook-addon-mock": "^5.0.0",
"storybook": "^7.6.17",
"storybook-addon-mock": "^4.3.0",
"ts-lit-plugin": "^2.0.2",
"tslib": "^2.6.2",
"turnstile-types": "^1.2.1",

View File

@ -166,7 +166,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
${this.renderNewVersionMessage()}
${this.renderImpersonationMessage()}
${map(sidebarContent, renderOneSidebarItem)}
${this.renderEnterpriseMenu()}
${this.renderEnterpriseMessage()}
`;
}
@ -199,7 +199,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
: nothing;
}
renderEnterpriseMenu() {
renderEnterpriseMessage() {
return this.can(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>

View File

@ -3,8 +3,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/chips/Chip";
import "@goauthentik/elements/chips/ChipGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -14,17 +12,22 @@ import YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { CoreApi, CoreGroupsListRequest, Group, RbacApi, Role } from "@goauthentik/api";
export function rbacRolePair(item: Role): DualSelectPair {
return [item.pk, html`<div class="selection-main">${item.name}</div>`, item.name];
}
import {
CoreApi,
CoreGroupsListRequest,
Group,
PaginatedRoleList,
RbacApi,
} from "@goauthentik/api";
@customElement("ak-group-form")
export class GroupForm extends ModelForm<Group, string> {
@state()
roles?: PaginatedRoleList;
static get styles(): CSSResult[] {
return super.styles.concat(css`
.pf-c-button.pf-m-control {
@ -48,6 +51,12 @@ export class GroupForm extends ModelForm<Group, string> {
: msg("Successfully created group.");
}
async load(): Promise<void> {
this.roles = await new RbacApi(DEFAULT_CONFIG).rbacRolesList({
ordering: "name",
});
}
async send(data: Group): Promise<Group> {
if (this.instance?.pk) {
return new CoreApi(DEFAULT_CONFIG).coreGroupsPartialUpdate({
@ -118,29 +127,24 @@ export class GroupForm extends ModelForm<Group, string> {
</ak-search-select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Roles")} name="roles">
<ak-dual-select-provider
.provider=${(page: number, search?: string): Promise<DataProvision> => {
return new RbacApi(DEFAULT_CONFIG)
.rbacRolesList({
page: page,
search: search,
})
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(rbacRolePair),
};
});
}}
.selected=${(this.instance?.rolesObj ?? []).map(rbacRolePair)}
available-label="${msg("Available Roles")}"
selected-label="${msg("Selected Roles")}"
></ak-dual-select-provider>
<select class="pf-c-form-control" multiple>
${this.roles?.results.map((role) => {
const selected = Array.from(this.instance?.roles || []).some((sp) => {
return sp == role.pk;
});
return html`<option value=${role.pk} ?selected=${selected}>
${role.name}
</option>`;
})}
</select>
<p class="pf-c-form__helper-text">
${msg(
"Select roles to grant this groups' users' permissions from the selected roles.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Attributes")}

View File

@ -110,7 +110,10 @@ export class UserWriteStageForm extends BaseStageForm<UserWriteStage> {
${msg("Mark newly created users as inactive.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("User type")} name="userType">
<ak-form-element-horizontal
label=${msg("User path template")}
name="userPathTemplate"
>
<ak-radio
.options=${[
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 699 KiB

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.4.0";
export const VERSION = "2024.2.2";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -1,6 +1,5 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { globalAK } from "@goauthentik/common/global";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { ContextProvider } from "@lit/context";
@ -24,9 +23,6 @@ export class ConfigContextController implements ReactiveController {
context: authentikConfigContext,
initialValue: undefined,
});
// Pre-hydrate from template-embedded config
this.context.setValue(globalAK().config);
this.host.config = globalAK().config;
this.fetch = this.fetch.bind(this);
this.fetch();
}

View File

@ -13,7 +13,7 @@ import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -107,23 +107,21 @@ export class PageHeader extends WithBrandConfig(AKElement) {
});
}
setTitle(header?: string) {
setTitle(value: string) {
const currentIf = currentInterface();
let title = this.brand?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") {
title = `${msg("Admin")} - ${title}`;
}
// Prepend the header to the title
if (header !== undefined && header !== "") {
title = `${header} - ${title}`;
}
document.title = title;
const title = this.brand?.brandingTitle || TITLE_DEFAULT;
document.title =
currentIf === "admin"
? `${msg("Admin")} - ${title}`
: value !== ""
? `${value} - ${title}`
: title;
}
willUpdate() {
// Always update title, even if there's no header value set,
// as in that case we still need to return to the generic title
this.setTitle(this.header);
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("header") && this.header) {
this.setTitle(this.header);
}
}
renderIcon(): TemplateResult {

View File

@ -1,15 +1,18 @@
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { EVENT_LOCALE_CHANGE } from "@goauthentik/common/constants";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { customEvent, isCustomEvent } from "@goauthentik/elements/utils/customEvents";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { customElement, property } from "lit/decorators.js";
import { WithBrandConfig } from "../Interface/brandProvider";
import { initializeLocalization } from "./configureLocale";
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
const LocaleContextBase = WithBrandConfig(LitElement);
import {
DEFAULT_LOCALE,
autoDetectLanguage,
getBestMatchLocale,
localeCodeFromUrl,
} from "./helpers";
/**
* A component to manage your locale settings.
@ -25,7 +28,7 @@ const LocaleContextBase = WithBrandConfig(LitElement);
* @fires ak-locale-change - When a valid locale has been swapped in
*/
@customElement("ak-locale-context")
export class LocaleContext extends LocaleContextBase {
export class LocaleContext extends LitElement {
/// @attribute The text representation of the current locale */
@property({ attribute: true, type: String })
locale = DEFAULT_LOCALE;
@ -38,9 +41,6 @@ export class LocaleContext extends LocaleContextBase {
setLocale: LocaleSetter;
@state()
userLocale = "";
constructor(code = DEFAULT_LOCALE) {
super();
this.notifyApplication = this.notifyApplication.bind(this);
@ -59,15 +59,8 @@ export class LocaleContext extends LocaleContextBase {
connectedCallback() {
super.connectedCallback();
// Commenting out until we can come up with a better way of separating the
// "request user identity" with the session expiration heartbeat.
/*
new CoreApi(DEFAULT_CONFIG)
.coreUsersMeRetrieve()
.then((user) => (this.userLocale = user?.user?.settings?.locale ?? ""))
.catch(() => {});
*/
this.updateLocale();
const localeRequest = autoDetectLanguage(this.locale);
this.updateLocale(localeRequest);
window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler);
}
@ -76,19 +69,28 @@ export class LocaleContext extends LocaleContextBase {
super.disconnectedCallback();
}
updateLocaleHandler(_ev: Event) {
updateLocaleHandler(ev: Event) {
if (!isCustomEvent(ev)) {
console.warn(`Received a non-custom event at EVENT_LOCALE_REQUEST: ${ev}`);
return;
}
console.debug("authentik/locale: Locale update request received.");
this.updateLocale();
this.updateLocale(ev.detail.locale);
}
updateLocale() {
const localeRequest = autoDetectLanguage(this.userLocale, this.brand?.defaultLocale);
const locale = getBestMatchLocale(localeRequest);
updateLocale(code: string) {
const urlCode = localeCodeFromUrl(this.param);
const requestedLocale = urlCode ? urlCode : code;
const locale = getBestMatchLocale(requestedLocale);
if (!locale) {
console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
console.warn(`authentik/locale: failed to find locale for code ${code}`);
return;
}
locale.locale().then(() => {
console.debug(`authentik/locale: Loaded locale '${code}'`);
if (this.getLocale() === code) {
return;
}
console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`);
this.setLocale(locale.code).then(() => {
window.setTimeout(this.notifyApplication, 0);

View File

@ -45,13 +45,12 @@ export function localeCodeFromUrl(param = "locale") {
const isLocaleCandidate = (v: unknown): v is string =>
typeof v === "string" && v !== "" && v !== TOMBSTONE;
export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string {
export function autoDetectLanguage(requestedCode?: string): string {
const localeCandidates: string[] = [
localeCodeFromUrl("locale"),
userReq,
window.navigator?.language ?? TOMBSTONE,
brandReq,
globalAK()?.locale ?? TOMBSTONE,
localeCodeFromUrl("locale"),
requestedCode ?? TOMBSTONE,
window.navigator?.language ?? TOMBSTONE,
DEFAULT_LOCALE,
].filter(isLocaleCandidate);

View File

@ -28,11 +28,11 @@ export class LogViewer extends Table<LogEvent> {
pagination: {
next: 0,
previous: 0,
count: this.logs?.length || 0,
count: 1,
current: 1,
totalPages: 1,
startIndex: 1,
endIndex: this.logs?.length || 0,
endIndex: 1,
},
results: this.logs || [],
};

View File

@ -208,7 +208,7 @@ export class SidebarItem extends AKElement {
}
renderWithLabel() {
return html`
html`
<span class="pf-c-nav__link">
<slot name="label"></slot>
</span>

View File

@ -516,7 +516,7 @@ export class FlowExecutor extends Interface implements StageHost {
? html`
<li>
<a
href="https://unsplash.com/@sorasagano"
href="https://unsplash.com/@theforestbirds"
>${msg("Background image")}</a
>
</li>

View File

@ -6531,9 +6531,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -6800,9 +6800,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -6448,9 +6448,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -8447,158 +8447,117 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit>
<trans-unit id="s00bcaadd620a11f8">
<source>Latest version unknown</source>
<target>Dernière version inconnue</target>
</trans-unit>
<trans-unit id="s10fedec3a779ea63">
<source>Timestamp</source>
<target>Horodatage</target>
</trans-unit>
<trans-unit id="s48e186fb300e5464">
<source>Time</source>
<target>Temps</target>
</trans-unit>
<trans-unit id="saa8aa81ad6f055fd">
<source>Level</source>
<target>Niveau</target>
</trans-unit>
<trans-unit id="sd0ea7366ebc403ff">
<source>Event</source>
<target>Évènement</target>
</trans-unit>
<trans-unit id="s225f3e57c6a0d635">
<source>Logger</source>
<target>Logger</target>
</trans-unit>
<trans-unit id="s39d6971531a270b6">
<source>Update internal password on login</source>
<target>Mettre à jour le mot de passe interne à la connexion</target>
</trans-unit>
<trans-unit id="sefe04e319223a3d6">
<source>When the user logs in to authentik using this source password backend, update their credentials in authentik.</source>
<target>Lorsqu'un utilisateur se connecte à authentik en utilisant le backend de mot de passe de cette source, mettre à jour ses identifiants dans authentik.</target>
</trans-unit>
<trans-unit id="s5f43af3669b1e098">
<source>Source</source>
<target>Source</target>
</trans-unit>
<trans-unit id="sd9f752de8448bcc9">
<source>Resume timeout</source>
<target>Délai de reprise</target>
</trans-unit>
<trans-unit id="s517954806d7610f2">
<source>Amount of time a user can take to return from the source to continue the flow.</source>
<target>Durée que l'utilisateur peut prendre pour revenir de la source pour continuer le flux.</target>
</trans-unit>
<trans-unit id="s8c4bc6e515949112">
<source>Your Install ID</source>
<target>Votre ID d'installation</target>
</trans-unit>
<trans-unit id="s8cc0075913c67566">
<source>Enter the email associated with your account, and we'll send you a link to reset your password.</source>
<target>Entrer l'email associé à votre compte, et nous vous enverrons un lien pour réinitialiser votre mot de passe.</target>
</trans-unit>
<trans-unit id="s06bfe45ffef2cf60">
<source>Stage name: <x id="0" equiv-text="${this.challenge.name}"/></source>
<target>Nom de l'étape : <x id="0" equiv-text="${this.challenge.name}"/></target>
</trans-unit>
<trans-unit id="s90064dd5c4dde2c6">
<source>Please scan the QR code above using the Microsoft Authenticator, Google Authenticator, or other authenticator apps on your device, and enter the code the device displays below to finish setting up the MFA device.</source>
<target>Merci de scanner le QR code ci-dessus avec Microsoft Authenticator, Google Authenticator ou une autre application d'authentification à deux facteurs sur votre appareil, et entrer le code que l'appareil affiche ci-dessous pour finir la configuration de votre appareil MFA.</target>
</trans-unit>
<trans-unit id="s02160dc6adba3456">
<source>Inject an OAuth or SAML Source into the flow execution. This allows for additional user verification, or to dynamically access different sources for different user identifiers (username, email address, etc).</source>
<target>Injecte une source OAuth ou SAML dans lexécution du flux. Cela permet d'effectuer des vérifications additionnelles sur l'utilisateur, ou d'accéder dynamiquement à plusieurs sources pour des identifiants utilisateurs différents (nom d'utilisateur, adresse courriel, etc).</target>
</trans-unit>
<trans-unit id="sc7d071fb5cc1f6bf">
<source>A selection is required</source>
<target>Une sélection est requise</target>
</trans-unit>
<trans-unit id="sa64fb483becc9c2c">
<source>Device type restrictions</source>
<target>Restrictions de type d'appareil</target>
</trans-unit>
<trans-unit id="sbb928551c84cd63f">
<source>Available Device types</source>
<target>Types d'appareil disponibles</target>
</trans-unit>
<trans-unit id="s6446c35d6b411e53">
<source>Selected Device types</source>
<target>Types d'appareil sélectionnés</target>
</trans-unit>
<trans-unit id="s1f4df216b56de4ac">
<source>Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed.</source>
<target>Optionnel, restreindre quels types d'appareil WebAuthn peuvent être utilisés. Lorsqu'aucun type d'appareil n'est sélectionné, tout les appareils sont autorisés.</target>
</trans-unit>
<trans-unit id="s23ed998c51cbe38f">
<source>If the user has successfully authenticated with a device in the classes listed above within this configured duration, this stage will be skipped.</source>
<target>Si un utilisateur s'est authentifié avec succès avec un type d'appareil contenu dans ceux listés ci-dessus dans le temps configuré ici, cette étape sera passée.</target>
</trans-unit>
<trans-unit id="s4d4f52ccfd9d6adf">
<source>WebAuthn-specific settings</source>
<target>Réglages spécifiques à WebAuthn</target>
</trans-unit>
<trans-unit id="sb46d2e64f64c6284">
<source>WebAuthn Device type restrictions</source>
<target>Restrictions de type d'appareil WebAuthn</target>
</trans-unit>
<trans-unit id="s11274960b13cf21a">
<source>This restriction only applies to devices created in authentik 2024.4 or later.</source>
<target>Les restrictions ne s'appliquent qu'aux appareils créés dans authentik 2024.4 ou ultérieur.</target>
</trans-unit>
<trans-unit id="s4888252cbd785175">
<source>Default token duration</source>
<target>Durée par défaut des jetons</target>
</trans-unit>
<trans-unit id="s25c80a16a07871ba">
<source>Default duration for generated tokens</source>
<target>Durée par défaut des jetons générés</target>
</trans-unit>
<trans-unit id="se5c9ffc025fdf61d">
<source>Default token length</source>
<target>Longueur par défaut des jetons</target>
</trans-unit>
<trans-unit id="sa6b2c110466d1754">
<source>Default length of generated tokens</source>
<target>Longueur par défaut des jetons générés</target>
</trans-unit>
<trans-unit id="s1f25e1e20469a9ea">
<source>deleted</source>
<target>supprimé</target>
</trans-unit>
<trans-unit id="s0ca04e397298bc43">
<source>Select permissions to assign</source>
<target>Sélectionner les permissions à assigner</target>
</trans-unit>
<trans-unit id="s676d94e7e31a8075">
<source>SCIM Source is in preview.</source>
<target>La source SCIM est en aperçu.</target>
</trans-unit>
<trans-unit id="s31f1afc0a81977c1">
<source>Update SCIM Source</source>
<target>Mettre à jour la source SCIM</target>
</trans-unit>
<trans-unit id="s4bb356adc8a7f85b">
<source>SCIM Base URL</source>
<target>URL de base SCIM</target>
</trans-unit>
<trans-unit id="sb23304fc42c5d6d9">
<source>Provisioned Users</source>
<target>Utilisateurs provisionnés</target>
</trans-unit>
<trans-unit id="s6a81ee82b2e5ecbb">
<source>Provisioned Groups</source>
<target>Groupes provisionnés</target>
</trans-unit>
<trans-unit id="s10154dbd4fbc697b">
<source>removed</source>
<target>supprimé</target>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
<target>Vérification...</target>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -8386,9 +8386,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -8230,9 +8230,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -6652,9 +6652,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -8502,7 +8502,4 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body></file></xliff>

View File

@ -6441,9 +6441,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -5359,9 +5359,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -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 &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source>
<target>未找到 URL &quot;
<x id="0" equiv-text="${this.url}"/>&quot;。</target>
</trans-unit>
<trans-unit id="s58cd9c2fe836d9c6">
@ -1040,8 +1040,8 @@
</trans-unit>
<trans-unit id="sa8384c9c26731f83">
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
<source>To allow any redirect URI, set this value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 &quot;.*&quot;。请注意这可能带来的安全影响。</target>
</trans-unit>
<trans-unit id="s55787f4dfcdce52b">
@ -1782,8 +1782,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 &quot;fa-test&quot;.</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 &quot;fa-test&quot;。</target>
</trans-unit>
<trans-unit id="s0410779cb47de312">
@ -2961,8 +2961,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s76768bebabb7d543">
<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,...'</source>
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
<source>Field which contains members of a group. Note that if using the &quot;memberUid&quot; field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 &quot;memberUid&quot; 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
</trans-unit>
<trans-unit id="s026555347e589f0e">
@ -3739,8 +3739,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s7b1fba26d245cb1c">
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
<source>When using an external logging solution for archiving, this can be set to &quot;minutes=5&quot;.</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 &quot;minutes=5&quot;。</target>
</trans-unit>
<trans-unit id="s44536d20bb5c8257">
@ -3916,10 +3916,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}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</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}"/>&quot;
<x id="1" equiv-text="${this.obj?.name}"/>&quot; 吗?</target>
</trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6">
@ -4995,7 +4995,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 &quot;roaming&quot; authenticator, like a YubiKey</source>
<target>像 YubiKey 这样的“漫游”身份验证器</target>
</trans-unit>
@ -5330,10 +5330,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<source><x id="0" equiv-text="${prompt.name}"/> (&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<target>
<x id="0" equiv-text="${prompt.name}"/>"
<x id="1" equiv-text="${prompt.fieldKey}"/>",类型为
<x id="0" equiv-text="${prompt.name}"/>&quot;
<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;,类型为
<x id="2" equiv-text="${prompt.type}"/></target>
</trans-unit>
@ -5382,7 +5382,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 &quot;stay signed in&quot;, which will extend their session by the time specified here.</source>
<target>如果设置时长大于 0用户可以选择“保持登录”选项这将使用户的会话延长此处设置的时间。</target>
</trans-unit>
@ -7830,7 +7830,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 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</source>
<target>此用户将会被添加到组 &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;。</target>
</trans-unit>
<trans-unit id="s62e7f6ed7d9cb3ca">
@ -8598,10 +8598,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
<target>正在验证...</target>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>
</xliff>
</xliff>

View File

@ -6489,9 +6489,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -8347,9 +8347,6 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s30d6ff9e15e0a40a">
<source>Verifying...</source>
</trans-unit>
<trans-unit id="sc1673c93148583ba">
<source>Request failed. Please try again later.</source>
</trans-unit>
</body>
</file>

View File

@ -1,15 +0,0 @@
---
title: API Clients
---
These API clients are officially supported and maintained.
:::info
These API clients are primarily built around creating/updating/deleting configuration objects in authentik, and in most cases can **not** be used to implemented SSO into your application.
:::
| Language | Package name | URL |
| --------------------- | ----------------------- | ---------------------------------------------- |
| JavaScript/Typescript | `@goauthentik/api` | https://www.npmjs.com/package/@goauthentik/api |
| Go | `goauthentik.io/api/v3` | https://pkg.go.dev/goauthentik.io/api/v3 |
| Python | `authentik_client` | https://pypi.org/project/authentik-client/ |

View File

@ -58,11 +58,3 @@ When enabled, all the events caused by a user will be deleted upon the user's de
### Impersonation
Globally enable/disable impersonation. Defaults to `true`.
### Default token duration
Default duration for generated tokens. Defaults to `minutes=30`.
### Default token length
Default length of generated tokens. Defaults to 60.

View File

@ -21,9 +21,7 @@ Using the `Not configured action`, you can choose what happens when a user does
By default, authenticator validation is required every time the flow containing this stage is executed. To only change this behavior, set _Last validation threshold_ to a non-zero value. (Requires authentik 2022.5)
Keep in mind that when using Code-based devices (TOTP, Static and SMS), values lower than `seconds=30` cannot be used, as with the way TOTP devices are saved, there is no exact timestamp.
### Options
#### Less-frequent validation
### Less-frequent validation
:::info
Requires authentik 2022.5.1
@ -31,13 +29,12 @@ Requires authentik 2022.5.1
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the users devices within the selected classes are checked.
#### Passwordless authentication
### Passwordless authentication
:::info
Requires authentik 2021.12.4
:::
:::caution
:::danger
Firefox has some known issues regarding FIDO (see https://bugzilla.mozilla.org/show_bug.cgi?id=1530370) and TouchID (see https://bugzilla.mozilla.org/show_bug.cgi?id=1536482)
:::
@ -51,7 +48,7 @@ As final stage, bind a _User login_ stage.
Users can either access this flow directly via its URL, or you can modify any Identification stage's _Passwordless flow_ setting to add a direct link to this flow.
#### Logging
### Logging
Logins which used Passwordless authentication have the _auth_method_ context variable set to `auth_webauthn_pwl`, and the device used is saved in the arguments. Example:
@ -76,7 +73,7 @@ Logins which used Passwordless authentication have the _auth_method_ context var
}
```
#### WebAuthn Device type restrictions
### `WebAuthn Device type restrictions`
:::info
Requires authentik 2024.4

View File

@ -4,21 +4,19 @@ title: WebAuthn authenticator setup stage
This stage configures a WebAuthn-based Authenticator. This can either be a browser, biometrics or a Security stick like a YubiKey.
### Options
#### User verification
### `User verification`
Configure if authentik should require, prefer or discourage user verification for the authenticator. For example when using a virtual authenticator like Windows Hello, this setting controls if a PIN is required.
#### Resident key requirement
### `Resident key requirement`
Configure if the created authenticator is stored in the encrypted memory on the device or in persistent memory. When configuring [passwordless login](../identification/index.md#passwordless-flow), this should be set to either _Preferred_ or _Required_, otherwise the authenticator cannot be used for passwordless authentication.
#### Authenticator Attachment
### `Authenticator Attachment`
Configure if authentik will require either a removable device (like a YubiKey, Google Titan, etc) or a non-removable device (like Windows Hello, TouchID or password managers), or not send a requirement.
#### Device type restrictions
### `Device type restrictions`
:::info
Requires authentik 2024.4

View File

@ -98,8 +98,8 @@ Templates are rendered using Django's templating engine. The following variables
{% block content %}
<tr>
<td class="alert alert-success">
{% blocktrans with username=user.username %} Hi {{ username }},
{% endblocktrans %}
{% blocktrans with username=user.username %} Hi {{ username }}, {%
endblocktrans %}
</td>
</tr>
<tr>

View File

@ -38,20 +38,12 @@ It is very important that the configured source's authentication and enrollment
This is because the Source stage works by appending a [dynamic in-memory](../../../core/terminology.md#dynamic-in-memory-stage) stage to the source's flow, so having a [User login stage](../user_login/index.md) bound will cause the source's flow to not resume the original flow it was started from, and instead directly authenticating the pending user.
### Example use case
This stage can be used to leverage an external OAuth/SAML identity provider.
For example, you can authenticate users by routing them through a custom device-health solution.
Another use case is to route users to authenticate with your legacy (Okta, etc) IdP and then use the returned identity and attributes within authentik as part of an authorization flow, for example as part of an IdP migration. For authentication/enrollment this is also possible with an [OAuth](../../../../integrations/sources/oauth/)/[SAML](../../../../integrations/sources/saml/) source by itself.
### Options
#### Source
#### `source`
The source the user is redirected to. Must be a web-based source, such as [OAuth](../../../../integrations/sources/oauth/) or [SAML](../../../../integrations/sources/saml/). Sources like [LDAP](../../../../integrations/sources/ldap/) are _not_ compatible.
#### Resume timeout
#### `resume_timeout`
Because the execution of the current flow is suspended before the user is redirected to the configured source, this option configures how long the suspended flow is saved. If this timeout is exceeded, upon return from the configured source, the suspended flow will restart from the beginning.

View File

@ -72,7 +72,7 @@ To check if your config has been applied correctly, you can run the following co
- `AUTHENTIK_POSTGRESQL__PASSWORD`: Database password, defaults to the environment variable `POSTGRES_PASSWORD`
- `AUTHENTIK_POSTGRESQL__USE_PGBOUNCER`: Adjust configuration to support connection to PgBouncer
- `AUTHENTIK_POSTGRESQL__USE_PGPOOL`: Adjust configuration to support connection to Pgpool
- `AUTHENTIK_POSTGRESQL__SSLMODE`: Strictness of ssl verification. Defaults to `"verify-ca"`
- `AUTHENTIK_POSTGRESQL__SSLMODE`: Strictness of ssl verification. Defaults to `verify-ca`
- `AUTHENTIK_POSTGRESQL__SSLROOTCERT`: CA root for server ssl verification
- `AUTHENTIK_POSTGRESQL__SSLCERT`: Path to x509 client certificate to authenticate to server
- `AUTHENTIK_POSTGRESQL__SSLKEY`: Path to private key of `SSLCERT` certificate
@ -85,8 +85,7 @@ To check if your config has been applied correctly, you can run the following co
- `AUTHENTIK_REDIS__USERNAME`: Redis server username when not using configuration URL
- `AUTHENTIK_REDIS__PASSWORD`: Redis server password when not using configuration URL
- `AUTHENTIK_REDIS__TLS`: Redis server connection using TLS when not using configuration URL
- `AUTHENTIK_REDIS__TLS_REQS`: Redis server TLS connection requirements when not using configuration URL. Defaults to `"none"`. Allowed values are `"none"` and `"required"`.
- `AUTHENTIK_REDIS__TLS_CA_CERT`: Path to the Redis server TLS CA root when not using configuration URL. Defaults to `null`.
- `AUTHENTIK_REDIS__TLS_REQS`: Redis server TLS connection requirements when not using configuration URL
## Result Backend Settings
@ -313,14 +312,6 @@ Configure how long reputation scores should be saved for in seconds. Note that t
Defaults to `86400`.
### `AUTHENTIK_SESSION_STORAGE`
:::info
Requires authentik 2024.4
:::
Configure if the sessions are stored in the cache or the database. Defaults to `cache`. Allowed values are `cache` and `db`. Note that changing this value will invalidate all previous sessions.
### `AUTHENTIK_WEB__WORKERS`
:::info

View File

@ -51,7 +51,7 @@ Run the following commands to generate a password and secret key and write them
{/* prettier-ignore */}
```shell
echo "PG_PASS=$(openssl rand -base64 36)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 36)" >> .env
```
:::info

View File

@ -23,9 +23,9 @@ password=my-token
This will return a JSON response with an `access_token`, which is a signed JWT token. This token can be sent along requests to other hosts, which can then validate the JWT based on the signing key configured in authentik.
Starting with authentik 2024.4, it is also possible to encode the username and token of the user to authenticate with, separated with a colon, into a base64 string and pass it as `client_secret` value.
Starting with authentik 2024.next, it is also possible to encode the username and token of the user to authenticate with, separated with a colon, into a base64 string and pass it as `client_secret` value.
In addition to that, with authentik 2024.4 it is also possible to pass the configured `client_secret` value, which will automatically generate a service account user for which the JWT token will be issued.
In addition to that, with authentik 2024.next it is also possible to pass the configured `client_secret` value, which will automatically generate a service account user for which the JWT token will be issued.
### JWT-authentication

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