Compare commits

..

24 Commits

Author SHA1 Message Date
3925f5a208 release: 2023.5.6 2023-08-29 19:36:52 +02:00
6add4a62b9 include cure53 report
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-08-29 19:35:50 +02:00
54d5aa20ba security: fix CVE-2023-39522 (#6665)
* stages/email: don't disclose whether a user exists or not when recovering

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

* update website

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2023/v2023.5.md
#	website/docs/releases/2023/v2023.6.md
2023-08-29 19:08:47 +02:00
b99ac01228 release: 2023.5.5 2023-07-06 18:15:56 +02:00
15026748d1 security: fix CVE-2023-36456
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	website/sidebars.js
2023-07-06 18:15:46 +02:00
2739376a2a release: 2023.5.4 2023-06-22 21:45:33 +02:00
152121175b bump web api client
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-22 21:33:02 +02:00
1d57a258f3 ATH-01-012: escape quotation marks
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:48:08 +02:00
f15cac39c8 ATH-01-014: save authenticator validation state in flow context
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

bugfixes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:48:05 +02:00
ce77d82b24 ATH-01-010: rework
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:48:03 +02:00
c3fe57197d ATH-01-009: migrate impersonation to use API
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	authentik/core/urls.py
#	web/src/admin/AdminInterface.ts
#	web/src/admin/users/RelatedUserList.ts
#	web/src/admin/users/UserListPage.ts
#	web/src/admin/users/UserViewPage.ts
#	web/src/user/UserInterface.ts

# Conflicts:
#	authentik/core/urls.py
2023-06-19 13:47:53 +02:00
267938d435 ATH-01-005: use hmac.compare_digest for secret_key authentication
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:47:11 +02:00
6a7c2e0662 ATH-01-003 / ATH-01-012: disable htmlLabels in mermaid
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:47:09 +02:00
5336afb1b4 ATH-01-004: remove env from admin system endpoint
this endpoint already required admin access, but for debugging the env variables are used very little

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:47:06 +02:00
9bb44055a3 ATH-01-008: fix web forms not submitting correctly when pressing enter
When submitting some forms with the Enter key instead of clicking "Confirm"/etc, the form would not get submitted correctly

This would in the worst case is when setting a user's password, where the new password can end up in the URL, but the password was not actually saved to the user.

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

# Conflicts:
#	web/src/admin/applications/ApplicationCheckAccessForm.ts
#	web/src/admin/crypto/CertificateGenerateForm.ts
#	web/src/admin/flows/FlowImportForm.ts
#	web/src/admin/groups/RelatedGroupList.ts
#	web/src/admin/policies/PolicyTestForm.ts
#	web/src/admin/property-mappings/PropertyMappingTestForm.ts
#	web/src/admin/providers/saml/SAMLProviderImportForm.ts
#	web/src/admin/users/RelatedUserList.ts
#	web/src/admin/users/ServiceAccountForm.ts
#	web/src/admin/users/UserPasswordForm.ts
#	web/src/admin/users/UserResetEmailForm.ts

# Conflicts:
#	web/src/admin/property-mappings/PropertyMappingTestForm.ts
2023-06-19 13:46:52 +02:00
143663d293 ATH-01-010: fix missing user filter for webauthn device
This prevents an attack that is only possible when an attacker can intercept HTTP traffic and in the case of HTTPS decrypt it.
2023-06-19 13:46:16 +02:00
bd54d034e1 ATH-01-001: resolve path and check start before loading blueprints
This is even less of an issue since 411ef239f6, since with that commit we only allow files that the listing returns

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:46:13 +02:00
be85eecac5 release: 2023.5.3 2023-06-01 19:35:13 +02:00
24385c9c68 ci: build outpost binaries statically linked (#5823) 2023-05-31 16:58:10 +02:00
e141a11475 blueprints: fix API validation with OCI blueprint path (#5822)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-31 14:52:12 +02:00
b055adec2a ci: replace github bot account with github app (#5819)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-05-31 14:52:09 +02:00
772acb10d6 providers/ldap: fix LDAP Outpost application selection (#5812)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-31 14:51:46 +02:00
a7bf963409 stages/deny: fix typos (#5800)
* Fix typo in stage.py

Fix typo in "Cancells the current flow"

Signed-off-by: rlew-is <96594816+rlew-is@users.noreply.github.com>

* Fix typo in models.py

Fix typo in "Cancells the current flow"

Signed-off-by: rlew-is <96594816+rlew-is@users.noreply.github.com>

---------

Signed-off-by: rlew-is <96594816+rlew-is@users.noreply.github.com>
2023-05-30 10:54:24 +02:00
317afc932a web/flows: fix RedirectStage not detecting absolute URLs correctly (#5781)
* web: getURL() method in RedirectStage.ts now actually detects URLs (#5732)

Signed-off-by: Saeverix <1863379+Saeverix@users.noreply.github.com>

* use native API to build full URL

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

---------

Signed-off-by: Saeverix <1863379+Saeverix@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-05-30 10:54:12 +02:00
141 changed files with 2612 additions and 4313 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2023.5.2
current_version = 2023.5.6
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -135,4 +135,5 @@ jobs:
set -x
export GOOS=${{ matrix.goos }}
export GOARCH=${{ matrix.goarch }}
export CGO_ENABLED=0
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}

View File

@ -10,6 +10,11 @@ jobs:
name: Delete old unused container images
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Delete 'dev' containers older than a week
uses: snok/container-retention-policy@v2
with:
@ -18,5 +23,5 @@ jobs:
account-type: org
org-name: goauthentik
untagged-only: false
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
skip-tags: gh-next,gh-main

View File

@ -123,6 +123,7 @@ jobs:
set -x
export GOOS=${{ matrix.goos }}
export GOARCH=${{ matrix.goarch }}
export CGO_ENABLED=0
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
- name: Upload binaries to release
uses: svenstaro/upload-release-action@v2

View File

@ -22,18 +22,23 @@ jobs:
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root server test-all
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Extract version number
id: get_version
uses: actions/github-script@v6
with:
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
github-token: ${{ steps.generate_token.outputs.token }}
script: |
return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
- name: Create Release
id: create_release
uses: actions/create-release@v1.1.4
env:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ steps.get_version.outputs.result }}

View File

@ -15,9 +15,14 @@ jobs:
compile:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v3
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run compile
@ -26,7 +31,7 @@ jobs:
uses: peter-evans/create-pull-request@v5
id: cpr
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
branch: compile-backend-translation
commit-message: "core: compile backend translations"
title: "core: compile backend translations"

View File

@ -9,9 +9,14 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v3
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
@ -33,7 +38,7 @@ jobs:
- uses: peter-evans/create-pull-request@v5
id: cpr
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
branch: update-web-api-client
commit-message: "web: bump API Client version"
title: "web: bump API Client version"
@ -44,6 +49,6 @@ jobs:
author: authentik bot <github-bot@goauthentik.io>
- uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
merge-method: squash

View File

@ -2,7 +2,7 @@
from os import environ
from typing import Optional
__version__ = "2023.5.2"
__version__ = "2023.5.6"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,5 +1,4 @@
"""authentik administration overview"""
import os
import platform
from datetime import datetime
from sys import version as python_version
@ -34,7 +33,6 @@ class RuntimeDict(TypedDict):
class SystemSerializer(PassiveSerializer):
"""Get system information."""
env = SerializerMethodField()
http_headers = SerializerMethodField()
http_host = SerializerMethodField()
http_is_secure = SerializerMethodField()
@ -43,10 +41,6 @@ class SystemSerializer(PassiveSerializer):
server_time = SerializerMethodField()
embedded_outpost_host = SerializerMethodField()
def get_env(self, request: Request) -> dict[str, str]:
"""Get Environment"""
return os.environ.copy()
def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers"""
headers = {}

View File

@ -1,4 +1,5 @@
"""API Authentication"""
from hmac import compare_digest
from typing import Any, Optional
from django.conf import settings
@ -78,7 +79,7 @@ def token_secret_key(value: str) -> Optional[User]:
and return the service account for the managed outpost"""
from authentik.outposts.apps import MANAGED_OUTPOST
if value != settings.SECRET_KEY:
if not compare_digest(value, settings.SECRET_KEY):
return None
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
if not outposts:

View File

@ -13,6 +13,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
@ -36,7 +37,7 @@ class BlueprintInstanceSerializer(ModelSerializer):
def validate_path(self, path: str) -> str:
"""Ensure the path (if set) specified is retrievable"""
if path == "":
if path == "" or path.startswith(OCI_PREFIX):
return path
files: list[dict] = blueprints_find_dict.delay().get()
if path not in [file["path"] for file in files]:

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog import get_logger
from authentik.blueprints.v1.oci import BlueprintOCIClient, OCIException
from authentik.blueprints.v1.oci import OCI_PREFIX, BlueprintOCIClient, OCIException
from authentik.lib.config import CONFIG
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.lib.sentry import SentryIgnoredException
@ -72,7 +72,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
def retrieve_oci(self) -> str:
"""Get blueprint from an OCI registry"""
client = BlueprintOCIClient(self.path.replace("oci://", "https://"))
client = BlueprintOCIClient(self.path.replace(OCI_PREFIX, "https://"))
try:
manifests = client.fetch_manifests()
return client.fetch_blobs(manifests)
@ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
def retrieve_file(self) -> str:
"""Get blueprint from path"""
try:
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
base = Path(CONFIG.y("blueprints_dir"))
full_path = base.joinpath(Path(self.path)).resolve()
if not str(full_path).startswith(str(base.resolve())):
raise BlueprintRetrievalFailed("Invalid blueprint path")
with full_path.open("r", encoding="utf-8") as _file:
return _file.read()
except (IOError, OSError) as exc:
@ -90,7 +93,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
def retrieve(self) -> str:
"""Retrieve blueprint contents"""
if self.path.startswith("oci://"):
if self.path.startswith(OCI_PREFIX):
return self.retrieve_oci()
if self.path != "":
return self.retrieve_file()

View File

@ -11,37 +11,31 @@ metadata:
entries:
- model: authentik_core.token
identifiers:
identifier: "%(uid)s-token"
identifier: %(uid)s-token
attrs:
key: "%(uid)s"
user: "%(user)s"
key: %(uid)s
user: %(user)s
intent: api
- model: authentik_core.application
identifiers:
slug: "%(uid)s-app"
slug: %(uid)s-app
attrs:
name: "%(uid)s-app"
name: %(uid)s-app
icon: https://goauthentik.io/img/icon.png
- model: authentik_sources_oauth.oauthsource
identifiers:
slug: "%(uid)s-source"
slug: %(uid)s-source
attrs:
name: "%(uid)s-source"
name: %(uid)s-source
provider_type: azuread
consumer_key: "%(uid)s"
consumer_secret: "%(uid)s"
consumer_key: %(uid)s
consumer_secret: %(uid)s
icon: https://goauthentik.io/img/icon.png
- model: authentik_flows.flow
identifiers:
slug: "%(uid)s-flow"
slug: %(uid)s-flow
attrs:
name: "%(uid)s-flow"
title: "%(uid)s-flow"
name: %(uid)s-flow
title: %(uid)s-flow
designation: authentication
background: https://goauthentik.io/img/icon.png
- model: authentik_core.user
identifiers:
username: "%(uid)s"
attrs:
name: "%(uid)s"
password: "%(uid)s"

View File

@ -1,34 +1,15 @@
"""authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
from authentik.lib.generators import generate_id
class TestModels(TestCase):
"""Test Models"""
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
"""Test serializer"""
def tester(self: TestModels):
if test_model._meta.abstract: # pragma: no cover
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
self.assertIsNotNone(model_class.serializer)
return tester
for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
def test_retrieve_file(self):
"""Test retrieve_file"""
instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts")
with self.assertRaises(BlueprintRetrievalFailed):
instance.retrieve()

View File

@ -0,0 +1,34 @@
"""authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
class TestModels(TestCase):
"""Test Models"""
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
"""Test serializer"""
def tester(self: TestModels):
if test_model._meta.abstract: # pragma: no cover
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
self.assertIsNotNone(model_class.serializer)
return tester
for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))

View File

@ -44,6 +44,14 @@ class TestBlueprintsV1API(APITestCase):
),
)
def test_api_oci(self):
"""Test validation with OCI path"""
res = self.client.post(
reverse("authentik_api:blueprintinstance-list"),
data={"name": "foo", "path": "oci://foo/bar"},
)
self.assertEqual(res.status_code, 201)
def test_api_blank(self):
"""Test blank"""
res = self.client.post(

View File

@ -2,7 +2,7 @@
from django.test import TransactionTestCase
from authentik.blueprints.v1.importer import Importer
from authentik.core.models import Application, Token, User
from authentik.core.models import Application, Token
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
@ -45,9 +45,3 @@ class TestBlueprintsV1ConditionalFields(TransactionTestCase):
flow = Flow.objects.filter(slug=f"{self.uid}-flow").first()
self.assertIsNotNone(flow)
self.assertEqual(flow.background, "https://goauthentik.io/img/icon.png")
def test_user(self):
"""Test user"""
user: User = User.objects.filter(username=self.uid).first()
self.assertIsNotNone(user)
self.assertTrue(user.check_password(self.uid))

View File

@ -19,6 +19,7 @@ from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import authentik_user_agent
OCI_MEDIA_TYPE = "application/vnd.goauthentik.blueprint.v1+yaml"
OCI_PREFIX = "oci://"
class OCIException(SentryIgnoredException):

View File

@ -28,6 +28,7 @@ from authentik.blueprints.models import (
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE
from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.events.monitored_tasks import (
MonitoredTask,
TaskResult,
@ -184,9 +185,9 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
instance: Optional[BlueprintInstance] = None
try:
instance: BlueprintInstance = BlueprintInstance.objects.filter(pk=instance_pk).first()
self.set_uid(slugify(instance.name))
if not instance or not instance.enabled:
return
self.set_uid(slugify(instance.name))
blueprint_content = instance.retrieve()
file_hash = sha512(blueprint_content.encode()).hexdigest()
importer = Importer(blueprint_content, instance.context)
@ -228,7 +229,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str):
def clear_failed_blueprints():
"""Remove blueprints which couldn't be fetched"""
# Exclude OCI blueprints as those might be temporarily unavailable
for blueprint in BlueprintInstance.objects.exclude(path__startswith="oci://"):
for blueprint in BlueprintInstance.objects.exclude(path__startswith=OCI_PREFIX):
try:
blueprint.retrieve()
except BlueprintRetrievalFailed:

View File

@ -1,4 +1,6 @@
"""Provider API Views"""
from django.db.models import QuerySet
from django.db.models.query import Q
from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
@ -56,17 +58,22 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
class ProviderFilter(FilterSet):
"""Filter for groups"""
"""Filter for providers"""
application__isnull = BooleanFilter(
field_name="application",
lookup_expr="isnull",
)
application__isnull = BooleanFilter(method="filter_application__isnull")
backchannel_only = BooleanFilter(
method="filter_backchannel_only",
)
def filter_backchannel_only(self, queryset, name, value):
def filter_application__isnull(self, queryset: QuerySet, name, value):
"""Only return providers that are neither assigned to application,
both as provider or application provider"""
return queryset.filter(
Q(backchannel_application__isnull=value, is_backchannel=True)
| Q(application__isnull=value)
)
def filter_backchannel_only(self, queryset: QuerySet, name, value):
"""Only return backchannel providers"""
return queryset.filter(is_backchannel=value)

View File

@ -33,7 +33,7 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)
self.fields["key"] = CharField()
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""

View File

@ -51,7 +51,6 @@ from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
from authentik.core.middleware import (
@ -68,11 +67,12 @@ from authentik.core.models import (
TokenIntents,
User,
)
from authentik.events.models import EventAction
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.config import CONFIG
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -113,30 +113,6 @@ class UserSerializer(ModelSerializer):
uid = CharField(read_only=True)
username = CharField(max_length=150, validators=[UniqueValidator(queryset=User.objects.all())])
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["password"] = CharField(required=False)
def create(self, validated_data: dict) -> User:
"""If this serializer is used in the blueprint context, we allow for
directly setting a password. However should be done via the `set_password`
method instead of directly setting it like rest_framework."""
instance: User = super().create(validated_data)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and "password" in validated_data:
instance.set_password(validated_data["password"])
instance.save()
return instance
def update(self, instance: User, validated_data: dict) -> User:
"""Same as `create` above, set the password directly if we're in a blueprint
context"""
instance = super().update(instance, validated_data)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and "password" in validated_data:
instance.set_password(validated_data["password"])
instance.save()
return instance
def validate_path(self, path: str) -> str:
"""Validate path"""
if path[:1] == "/" or path[-1] == "/":
@ -568,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet):
send_mails(email_stage, message)
return Response(status=204)
@permission_required("authentik_core.impersonate")
@extend_schema(
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
"401": OpenApiResponse(description="Access denied"),
},
)
@action(detail=True, methods=["POST"])
def impersonate(self, request: Request, pk: int) -> Response:
"""Impersonate a user"""
if not CONFIG.y_bool("impersonation"):
LOGGER.debug("User attempted to impersonate", user=request.user)
return Response(status=401)
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401)
user_to_be = self.get_object()
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return Response(status=201)
@extend_schema(
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
},
)
@action(detail=False, methods=["GET"])
def impersonate_end(self, request: Request) -> Response:
"""End Impersonation a user"""
if (
SESSION_KEY_IMPERSONATE_USER not in request.session
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return Response(status=204)
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_KEY_IMPERSONATE_USER]
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return Response(status=204)
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):

View File

@ -5,6 +5,7 @@ from typing import Any, Optional
from uuid import uuid4
from deepmerge import always_merger
from django.conf import settings
from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
@ -32,7 +33,6 @@ from authentik.lib.models import (
)
from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel
from authentik.root.install_id import get_install_id
LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
@ -217,7 +217,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
@property
def uid(self) -> str:
"""Generate a globally unique UID, based on the user ID and the hashed secret key"""
return sha256(f"{self.id}-{get_install_id()}".encode("ascii")).hexdigest()
return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
def locale(self, request: Optional[HttpRequest] = None) -> str:
"""Get the locale the user has configured"""

View File

@ -1,14 +1,14 @@
"""impersonation tests"""
from json import loads
from django.test.testcases import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
class TestImpersonation(TestCase):
class TestImpersonation(APITestCase):
"""impersonation tests"""
def setUp(self) -> None:
@ -23,10 +23,10 @@ class TestImpersonation(TestCase):
self.other_user.save()
self.client.force_login(self.user)
self.client.get(
self.client.post(
reverse(
"authentik_core:impersonate-init",
kwargs={"user_id": self.other_user.pk},
"authentik_api:user-impersonate",
kwargs={"pk": self.other_user.pk},
)
)
@ -35,7 +35,7 @@ class TestImpersonation(TestCase):
self.assertEqual(response_body["user"]["username"], self.other_user.username)
self.assertEqual(response_body["original"]["username"], self.user.username)
self.client.get(reverse("authentik_core:impersonate-end"))
self.client.get(reverse("authentik_api:user-impersonate-end"))
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
@ -46,9 +46,7 @@ class TestImpersonation(TestCase):
"""test impersonation without permissions"""
self.client.force_login(self.other_user)
self.client.get(
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
)
self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}))
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
@ -58,5 +56,5 @@ class TestImpersonation(TestCase):
"""test un-impersonation without impersonating first"""
self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_core:impersonate-end"))
self.assertRedirects(response, reverse("authentik_core:if-user"))
response = self.client.get(reverse("authentik_api:user-impersonate-end"))
self.assertEqual(response.status_code, 204)

View File

@ -16,7 +16,7 @@ from authentik.core.api.providers import ProviderViewSet
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.users import UserViewSet
from authentik.core.views import apps, impersonate
from authentik.core.views import apps
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView
@ -38,17 +38,6 @@ urlpatterns = [
apps.RedirectToAppLaunch.as_view(),
name="application-launch",
),
# Impersonation
path(
"-/impersonation/<int:user_id>/",
impersonate.ImpersonateInitView.as_view(),
name="impersonate-init",
),
path(
"-/impersonation/end/",
impersonate.ImpersonateEndView.as_view(),
name="impersonate-end",
),
# Interfaces
path(
"if/admin/",

View File

@ -1,60 +0,0 @@
"""authentik impersonation views"""
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.views import View
from structlog.stdlib import get_logger
from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
SESSION_KEY_IMPERSONATE_USER,
)
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
LOGGER = get_logger()
class ImpersonateInitView(View):
"""Initiate Impersonation"""
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
"""Impersonation handler, checks permissions"""
if not CONFIG.y_bool("impersonation"):
LOGGER.debug("User attempted to impersonate", user=request.user)
return HttpResponse("Unauthorized", status=401)
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return HttpResponse("Unauthorized", status=401)
user_to_be = get_object_or_404(User, pk=user_id)
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return redirect("authentik_core:if-user")
class ImpersonateEndView(View):
"""End User impersonation"""
def get(self, request: HttpRequest) -> HttpResponse:
"""End Impersonation handler"""
if (
SESSION_KEY_IMPERSONATE_USER not in request.session
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return redirect("authentik_core:if-user")
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_KEY_IMPERSONATE_USER]
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return redirect("authentik_core:root-redirect")

View File

@ -23,7 +23,8 @@ class DiagramElement:
style: list[str] = field(default_factory=lambda: ["[", "]"])
def __str__(self) -> str:
element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}'
description = self.description.replace('"', "#quot;")
element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}'
if self.action is not None:
if self.action != "":
element = f"--{self.action}--> {element}"

View File

@ -204,12 +204,12 @@ class ChallengeStageView(StageView):
for field, errors in response.errors.items():
for error in errors:
full_errors.setdefault(field, [])
full_errors[field].append(
{
"string": str(error),
"code": error.code,
}
)
field_error = {
"string": str(error),
}
if hasattr(error, "code"):
field_error["code"] = error.code
full_errors[field].append(field_error)
challenge_response.initial_data["response_errors"] = full_errors
if not challenge_response.is_valid():
self.logger.error(

View File

@ -23,7 +23,6 @@ from authentik.flows.api.bindings import FlowStageBindingSerializer
from authentik.flows.models import Flow
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
from authentik.root.install_id import get_install_id
class FlowInspectorPlanSerializer(PassiveSerializer):
@ -52,7 +51,7 @@ class FlowInspectorPlanSerializer(PassiveSerializer):
"""Get a unique session ID"""
request: Request = self.context["request"]
return sha256(
f"{request._request.session.session_key}-{get_install_id()}".encode("ascii")
f"{request._request.session.session_key}-{settings.SECRET_KEY}".encode("ascii")
).hexdigest()

View File

@ -5,18 +5,25 @@ postgresql:
name: authentik
user: authentik
port: 5432
password: 'env://POSTGRES_PASSWORD'
password: "env://POSTGRES_PASSWORD"
use_pgbouncer: false
listen:
listen_http: 0.0.0.0:9000
listen_https: 0.0.0.0:9443
listen_metrics: 0.0.0.0:9300
trusted_proxy_cidrs:
- 127.0.0.0/8
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- fe80::/10
- ::1/128
redis:
host: localhost
port: 6379
password: ''
password: ""
tls: false
tls_reqs: "none"
db: 0

View File

@ -16,10 +16,12 @@ LOGGER = get_logger()
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
"""Attempt to get the client's IP by checking common HTTP Headers.
Returns none if no IP Could be found"""
Returns none if no IP Could be found
No additional validation is done here as requests are expected to only arrive here
via the go proxy, which deals with validating these headers for us"""
headers = (
"HTTP_X_FORWARDED_FOR",
"HTTP_X_REAL_IP",
"REMOTE_ADDR",
)
for _header in headers:

View File

@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
)
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
http_request = self.factory.get(reverse("authentik_core:impersonate-end"))
http_request = self.factory.get(reverse("authentik_api:user-impersonate-end"))
http_request.user = self.user
http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end"))
http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
request = PolicyRequest(self.user)
request.set_http_request(http_request)

View File

@ -1,4 +1,8 @@
"""LDAPProvider API Views"""
from django.db.models import QuerySet
from django.db.models.query import Q
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@ -29,24 +33,41 @@ class LDAPProviderSerializer(ProviderSerializer):
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
class LDAPProviderFilter(FilterSet):
"""LDAP Provider filters"""
application__isnull = BooleanFilter(method="filter_application__isnull")
def filter_application__isnull(self, queryset: QuerySet, name, value):
"""Only return providers that are neither assigned to application,
both as provider or application provider"""
return queryset.filter(
Q(backchannel_application__isnull=value) | Q(application__isnull=value)
)
class Meta:
model = LDAPProvider
fields = {
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"base_dn": ["iexact"],
"search_group__group_uuid": ["iexact"],
"search_group__name": ["iexact"],
"certificate__kp_uuid": ["iexact"],
"certificate__name": ["iexact"],
"tls_server_name": ["iexact"],
"uid_start_number": ["iexact"],
"gid_start_number": ["iexact"],
}
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
"""LDAPProvider Viewset"""
queryset = LDAPProvider.objects.all()
serializer_class = LDAPProviderSerializer
filterset_fields = {
"application": ["isnull"],
"name": ["iexact"],
"authorization_flow__slug": ["iexact"],
"base_dn": ["iexact"],
"search_group__group_uuid": ["iexact"],
"search_group__name": ["iexact"],
"certificate__kp_uuid": ["iexact"],
"certificate__name": ["iexact"],
"tls_server_name": ["iexact"],
"uid_start_number": ["iexact"],
"gid_start_number": ["iexact"],
}
filterset_class = LDAPProviderFilter
search_fields = ["name"]
ordering = ["name"]

View File

@ -1,40 +1,185 @@
"""Kubernetes Traefik Middleware Reconciler"""
from dataclasses import asdict, dataclass, field
from typing import TYPE_CHECKING
from dacite.core import from_dict
from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.providers.proxy.controllers.k8s.traefik_2 import Traefik2MiddlewareReconciler
from authentik.providers.proxy.controllers.k8s.traefik_3 import (
Traefik3MiddlewareReconciler,
TraefikMiddleware,
)
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
class TraefikMiddlewareReconciler(KubernetesObjectReconciler):
@dataclass
class TraefikMiddlewareSpecForwardAuth:
"""traefik middleware forwardAuth spec"""
address: str
# pylint: disable=invalid-name
authResponseHeadersRegex: str = field(default="")
# pylint: disable=invalid-name
authResponseHeaders: list[str] = field(default_factory=list)
# pylint: disable=invalid-name
trustForwardHeader: bool = field(default=True)
@dataclass
class TraefikMiddlewareSpec:
"""Traefik middleware spec"""
# pylint: disable=invalid-name
forwardAuth: TraefikMiddlewareSpecForwardAuth
@dataclass
class TraefikMiddlewareMetadata:
"""Traefik Middleware metadata"""
name: str
namespace: str
labels: dict = field(default_factory=dict)
@dataclass
class TraefikMiddleware:
"""Traefik Middleware"""
# pylint: disable=invalid-name
apiVersion: str
kind: str
metadata: TraefikMiddlewareMetadata
spec: TraefikMiddlewareSpec
CRD_NAME = "middlewares.traefik.containo.us"
CRD_GROUP = "traefik.containo.us"
CRD_VERSION = "v1alpha1"
CRD_PLURAL = "middlewares"
class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]):
"""Kubernetes Traefik Middleware Reconciler"""
def __init__(self, controller: "KubernetesController") -> None:
super().__init__(controller)
self.reconciler = Traefik3MiddlewareReconciler(controller)
if not self.reconciler.crd_exists():
self.reconciler = Traefik2MiddlewareReconciler(controller)
self.api_ex = ApiextensionsV1Api(controller.client)
self.api = CustomObjectsApi(controller.client)
@property
def noop(self) -> bool:
return self.reconciler.noop
if not ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost],
mode__in=[ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN],
).exists():
self.logger.debug("No providers with forward auth enabled.")
return True
if not self._crd_exists():
self.logger.debug("CRD doesn't exist")
return True
return False
def _crd_exists(self) -> bool:
"""Check if the traefik middleware exists"""
return bool(
len(
self.api_ex.list_custom_resource_definition(
field_selector=f"metadata.name={CRD_NAME}"
).items
)
)
def reconcile(self, current: TraefikMiddleware, reference: TraefikMiddleware):
return self.reconcile(current, reference)
super().reconcile(current, reference)
if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
raise NeedsUpdate()
if (
current.spec.forwardAuth.authResponseHeadersRegex
!= reference.spec.forwardAuth.authResponseHeadersRegex
):
raise NeedsUpdate()
# Ensure all of our headers are set, others can be added by the user.
if not set(current.spec.forwardAuth.authResponseHeaders).issubset(
reference.spec.forwardAuth.authResponseHeaders
):
raise NeedsUpdate()
def get_reference_object(self) -> TraefikMiddleware:
return self.get_reference_object()
"""Get deployment object for outpost"""
return TraefikMiddleware(
apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
kind="Middleware",
metadata=TraefikMiddlewareMetadata(
name=self.name,
namespace=self.namespace,
labels=self.get_object_meta().labels,
),
spec=TraefikMiddlewareSpec(
forwardAuth=TraefikMiddlewareSpecForwardAuth(
address=(
f"http://{self.name}.{self.namespace}:9000/"
"outpost.goauthentik.io/auth/traefik"
),
authResponseHeaders=[
"X-authentik-username",
"X-authentik-groups",
"X-authentik-email",
"X-authentik-name",
"X-authentik-uid",
"X-authentik-jwt",
"X-authentik-meta-jwks",
"X-authentik-meta-outpost",
"X-authentik-meta-provider",
"X-authentik-meta-app",
"X-authentik-meta-version",
],
authResponseHeadersRegex="",
trustForwardHeader=True,
)
),
)
def create(self, reference: TraefikMiddleware):
return self.create(reference)
return self.api.create_namespaced_custom_object(
group=CRD_GROUP,
version=CRD_VERSION,
plural=CRD_PLURAL,
namespace=self.namespace,
body=asdict(reference),
field_manager=FIELD_MANAGER,
)
def delete(self, reference: TraefikMiddleware):
return self.delete(reference)
return self.api.delete_namespaced_custom_object(
group=CRD_GROUP,
version=CRD_VERSION,
namespace=self.namespace,
plural=CRD_PLURAL,
name=self.name,
)
def retrieve(self) -> TraefikMiddleware:
return self.retrieve()
return from_dict(
TraefikMiddleware,
self.api.get_namespaced_custom_object(
group=CRD_GROUP,
version=CRD_VERSION,
namespace=self.namespace,
plural=CRD_PLURAL,
name=self.name,
),
)
def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
return self.update(current, reference)
return self.api.patch_namespaced_custom_object(
group=CRD_GROUP,
version=CRD_VERSION,
namespace=self.namespace,
plural=CRD_PLURAL,
name=self.name,
body=asdict(reference),
field_manager=FIELD_MANAGER,
)

View File

@ -1,18 +0,0 @@
"""Kubernetes Traefik Middleware Reconciler"""
from typing import TYPE_CHECKING
from authentik.providers.proxy.controllers.k8s.traefik_3 import Traefik3MiddlewareReconciler
if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
class Traefik2MiddlewareReconciler(Traefik3MiddlewareReconciler):
"""Kubernetes Traefik Middleware Reconciler"""
def __init__(self, controller: "KubernetesController") -> None:
super().__init__(controller)
self.crd_name = "middlewares.traefik.containo.us"
self.crd_group = "traefik.containo.us"
self.crd_version = "v1alpha1"
self.crd_plural = "middlewares"

View File

@ -1,183 +0,0 @@
"""Kubernetes Traefik Middleware Reconciler"""
from dataclasses import asdict, dataclass, field
from typing import TYPE_CHECKING
from dacite.core import from_dict
from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
@dataclass
class TraefikMiddlewareSpecForwardAuth:
"""traefik middleware forwardAuth spec"""
address: str
# pylint: disable=invalid-name
authResponseHeadersRegex: str = field(default="")
# pylint: disable=invalid-name
authResponseHeaders: list[str] = field(default_factory=list)
# pylint: disable=invalid-name
trustForwardHeader: bool = field(default=True)
@dataclass
class TraefikMiddlewareSpec:
"""Traefik middleware spec"""
# pylint: disable=invalid-name
forwardAuth: TraefikMiddlewareSpecForwardAuth
@dataclass
class TraefikMiddlewareMetadata:
"""Traefik Middleware metadata"""
name: str
namespace: str
labels: dict = field(default_factory=dict)
@dataclass
class TraefikMiddleware:
"""Traefik Middleware"""
# pylint: disable=invalid-name
apiVersion: str
kind: str
metadata: TraefikMiddlewareMetadata
spec: TraefikMiddlewareSpec
class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]):
"""Kubernetes Traefik Middleware Reconciler"""
def __init__(self, controller: "KubernetesController") -> None:
super().__init__(controller)
self.api_ex = ApiextensionsV1Api(controller.client)
self.api = CustomObjectsApi(controller.client)
self.crd_name = "middlewares.traefik.io"
self.crd_group = "traefik.io"
self.crd_version = "v1alpha1"
self.crd_plural = "middlewares"
@property
def noop(self) -> bool:
if not ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost],
mode__in=[ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN],
).exists():
self.logger.debug("No providers with forward auth enabled.")
return True
if not self.crd_exists():
self.logger.debug("CRD doesn't exist")
return True
return False
def crd_exists(self) -> bool:
"""Check if the traefik middleware exists"""
return bool(
len(
self.api_ex.list_custom_resource_definition(
field_selector=f"metadata.name={self.crd_name}"
).items
)
)
def reconcile(self, current: TraefikMiddleware, reference: TraefikMiddleware):
super().reconcile(current, reference)
if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
raise NeedsUpdate()
if (
current.spec.forwardAuth.authResponseHeadersRegex
!= reference.spec.forwardAuth.authResponseHeadersRegex
):
raise NeedsUpdate()
# Ensure all of our headers are set, others can be added by the user.
if not set(current.spec.forwardAuth.authResponseHeaders).issubset(
reference.spec.forwardAuth.authResponseHeaders
):
raise NeedsUpdate()
def get_reference_object(self) -> TraefikMiddleware:
"""Get deployment object for outpost"""
return TraefikMiddleware(
apiVersion=f"{self.crd_group}/{self.crd_version}",
kind="Middleware",
metadata=TraefikMiddlewareMetadata(
name=self.name,
namespace=self.namespace,
labels=self.get_object_meta().labels,
),
spec=TraefikMiddlewareSpec(
forwardAuth=TraefikMiddlewareSpecForwardAuth(
address=(
f"http://{self.name}.{self.namespace}:9000/"
"outpost.goauthentik.io/auth/traefik"
),
authResponseHeaders=[
"X-authentik-username",
"X-authentik-groups",
"X-authentik-email",
"X-authentik-name",
"X-authentik-uid",
"X-authentik-jwt",
"X-authentik-meta-jwks",
"X-authentik-meta-outpost",
"X-authentik-meta-provider",
"X-authentik-meta-app",
"X-authentik-meta-version",
],
authResponseHeadersRegex="",
trustForwardHeader=True,
)
),
)
def create(self, reference: TraefikMiddleware):
return self.api.create_namespaced_custom_object(
group=self.crd_group,
version=self.crd_version,
plural=self.crd_plural,
namespace=self.namespace,
body=asdict(reference),
field_manager=FIELD_MANAGER,
)
def delete(self, reference: TraefikMiddleware):
return self.api.delete_namespaced_custom_object(
group=self.crd_group,
version=self.crd_version,
plural=self.crd_plural,
namespace=self.namespace,
name=self.name,
)
def retrieve(self) -> TraefikMiddleware:
return from_dict(
TraefikMiddleware,
self.api.get_namespaced_custom_object(
group=self.crd_group,
version=self.crd_version,
plural=self.crd_plural,
namespace=self.namespace,
name=self.name,
),
)
def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
return self.api.patch_namespaced_custom_object(
group=self.crd_group,
version=self.crd_version,
plural=self.crd_plural,
namespace=self.namespace,
name=self.name,
body=asdict(reference),
field_manager=FIELD_MANAGER,
)

View File

@ -90,7 +90,6 @@ class TestAuthNRequest(TestCase):
issuer="authentik",
pre_authentication_flow=create_test_flow(),
signing_kp=cert,
verification_kp=cert,
)
def test_signed_valid(self):

View File

@ -1,41 +0,0 @@
"""install ID"""
from functools import lru_cache
from uuid import uuid4
from psycopg2 import connect
from authentik.lib.config import CONFIG
@lru_cache
def get_install_id() -> str:
"""Get install ID of this instance. The method is cached as the install ID is
not expected to change"""
from django.conf import settings
from django.db import connection
if settings.TEST:
return str(uuid4())
with connection.cursor() as cursor:
cursor.execute("SELECT id FROM authentik_install_id LIMIT 1;")
return cursor.fetchone()[0]
@lru_cache
def get_install_id_raw():
"""Get install_id without django loaded, this is required for the startup when we get
the install_id but django isn't loaded yet and we can't use the function above."""
conn = connect(
dbname=CONFIG.y("postgresql.name"),
user=CONFIG.y("postgresql.user"),
password=CONFIG.y("postgresql.password"),
host=CONFIG.y("postgresql.host"),
port=int(CONFIG.y("postgresql.port")),
sslmode=CONFIG.y("postgresql.sslmode"),
sslrootcert=CONFIG.y("postgresql.sslrootcert"),
sslcert=CONFIG.y("postgresql.sslcert"),
sslkey=CONFIG.y("postgresql.sslkey"),
)
cursor = conn.cursor()
cursor.execute("SELECT id FROM authentik_install_id LIMIT 1;")
return cursor.fetchone()[0]

View File

@ -1,5 +1,4 @@
"""Dynamically set SameSite depending if the upstream connection is TLS or not"""
from functools import lru_cache
from hashlib import sha512
from time import time
from timeit import default_timer
@ -17,16 +16,10 @@ from jwt import PyJWTError, decode, encode
from structlog.stdlib import get_logger
from authentik.lib.utils.http import get_client_ip
from authentik.root.install_id import get_install_id
LOGGER = get_logger("authentik.asgi")
ACR_AUTHENTIK_SESSION = "goauthentik.io/core/default"
@lru_cache
def get_signing_hash():
"""Get cookie JWT signing hash"""
return sha512(get_install_id().encode()).hexdigest()
SIGNING_HASH = sha512(settings.SECRET_KEY.encode()).hexdigest()
class SessionMiddleware(UpstreamSessionMiddleware):
@ -54,7 +47,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
# for testing setups, where the session is directly set
session_key = key if settings.TEST else None
try:
session_payload = decode(key, get_signing_hash(), algorithms=["HS256"])
session_payload = decode(key, SIGNING_HASH, algorithms=["HS256"])
session_key = session_payload["sid"]
except (KeyError, PyJWTError):
pass
@ -121,7 +114,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
}
if request.user.is_authenticated:
payload["sub"] = request.user.uid
value = encode(payload=payload, key=get_signing_hash())
value = encode(payload=payload, key=SIGNING_HASH)
if settings.TEST:
value = request.session.session_key
response.set_cookie(

View File

@ -66,8 +66,8 @@ def ldap_sync_password(sender, user: User, password: str, **_):
if not sources.exists():
return
source = sources.first()
changer = LDAPPasswordChanger(source)
try:
changer = LDAPPasswordChanger(source)
changer.change_password(user, password)
except LDAPOperationResult as exc:
LOGGER.warning("failed to set LDAP password", exc=exc)

View File

@ -26,7 +26,6 @@ class SAMLSourceSerializer(SourceSerializer):
"allow_idp_initiated",
"name_id_policy",
"binding_type",
"verification_kp",
"signing_kp",
"digest_algorithm",
"signature_algorithm",
@ -56,7 +55,6 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
"allow_idp_initiated",
"name_id_policy",
"binding_type",
"verification_kp",
"signing_kp",
"digest_algorithm",
"signature_algorithm",

View File

@ -1,53 +0,0 @@
# Generated by Django 4.1.7 on 2023-05-19 21:55
import django.db.models.deletion
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_verification_cert(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
"""Migrate signing cert to verification_kp for backwards compat"""
SAMLSource = apps.get_model("authentik_sources_saml", "samlsource")
for source in SAMLSource.objects.using(schema_editor.connection.alias).all():
source.verification_kp = source.signing_kp
source.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0004_alter_certificatekeypair_name"),
("authentik_sources_saml", "0012_usersamlsourceconnection"),
]
operations = [
migrations.AddField(
model_name="samlsource",
name="verification_kp",
field=models.ForeignKey(
blank=True,
default=None,
help_text="When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="authentik_crypto.certificatekeypair",
verbose_name="Verification Certificate",
),
),
migrations.RunPython(migrate_verification_cert),
migrations.AlterField(
model_name="samlsource",
name="signing_kp",
field=models.ForeignKey(
blank=True,
default=None,
help_text="Keypair used to sign outgoing Responses going to the Identity Provider.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_crypto.certificatekeypair",
verbose_name="Signing Keypair",
),
),
]

View File

@ -121,27 +121,16 @@ class SAMLSource(Source):
),
)
verification_kp = models.ForeignKey(
CertificateKeyPair,
default=None,
null=True,
blank=True,
help_text=_(
"When selected, incoming assertion's Signatures will be validated against this "
"certificate. To allow unsigned Requests, leave on default."
),
on_delete=models.SET_NULL,
verbose_name=_("Verification Certificate"),
related_name="+",
)
signing_kp = models.ForeignKey(
CertificateKeyPair,
default=None,
null=True,
blank=True,
help_text=_("Keypair used to sign outgoing Responses going to the Identity Provider."),
on_delete=models.SET_NULL,
null=True,
verbose_name=_("Signing Keypair"),
help_text=_(
"Keypair which is used to sign outgoing requests. Leave empty to disable signing."
),
on_delete=models.SET_DEFAULT,
)
digest_algorithm = models.CharField(

View File

@ -72,7 +72,7 @@ class ResponseProcessor:
self._root_xml = b64decode(raw_response.encode())
self._root = fromstring(self._root_xml)
if self._source.verification_kp:
if self._source.signing_kp:
self._verify_signed()
self._verify_request_id()
self._verify_status()
@ -89,7 +89,7 @@ class ResponseProcessor:
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
self._source.verification_kp.certificate_data,
self._source.signing_kp.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
)
ctx.key = key

View File

@ -133,6 +133,12 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
if not device:
raise ValidationError("Invalid device")
# We can only check the device's user if the user we're given isn't anonymous
# as this validation is also used for password-less login where webauthn is the very first
# step done by a user. Only if this validation happens at a later stage we can check
# that the device belongs to the user
if not user.is_anonymous and device.user != user:
raise ValidationError("Invalid device")
stage: AuthenticatorValidateStage = stage_view.executor.current_stage

View File

@ -20,7 +20,6 @@ from authentik.flows.models import FlowDesignation, NotConfiguredAction, Stage
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.lib.utils.time import timedelta_from_string
from authentik.root.install_id import get_install_id
from authentik.stages.authenticator_sms.models import SMSDevice
from authentik.stages.authenticator_validate.challenge import (
DeviceChallenge,
@ -37,9 +36,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME
COOKIE_NAME_MFA = "authentik_mfa"
SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages"
SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage"
SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges"
PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages"
PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage"
PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges"
class SelectableStageSerializer(PassiveSerializer):
@ -73,8 +72,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-authenticator-validate")
def _challenge_allowed(self, classes: list):
device_challenges: list[dict] = self.stage.request.session.get(
SESSION_KEY_DEVICE_CHALLENGES, []
device_challenges: list[dict] = self.stage.executor.plan.context.get(
PLAN_CONTEXT_DEVICE_CHALLENGES, []
)
if not any(x["device_class"] in classes for x in device_challenges):
raise ValidationError("No compatible device class allowed")
@ -104,7 +103,9 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
# First check if the challenge is valid
allowed = False
for device_challenge in self.stage.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []):
for device_challenge in self.stage.executor.plan.context.get(
PLAN_CONTEXT_DEVICE_CHALLENGES, []
):
if device_challenge.get("device_class", "") == challenge.get(
"device_class", ""
) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
@ -122,11 +123,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
def validate_selected_stage(self, stage_pk: str) -> str:
"""Check that the selected stage is valid"""
stages = self.stage.request.session.get(SESSION_KEY_STAGES, [])
stages = self.stage.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
if not any(str(stage.pk) == stage_pk for stage in stages):
raise ValidationError("Selected stage is invalid")
self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
self.stage.request.session[SESSION_KEY_SELECTED_STAGE] = stage_pk
self.stage.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = stage_pk
return stage_pk
def validate(self, attrs: dict):
@ -231,7 +232,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
else:
self.logger.debug("No pending user, continuing")
return self.executor.stage_ok()
self.request.session[SESSION_KEY_DEVICE_CHALLENGES] = challenges
self.executor.plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = challenges
# No allowed devices
if len(challenges) < 1:
@ -264,23 +265,23 @@ class AuthenticatorValidateStageView(ChallengeStageView):
if stage.configuration_stages.count() == 1:
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
self.request.session[SESSION_KEY_SELECTED_STAGE] = next_stage
self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage
# Because that normal execution only happens on post, we directly inject it here and
# return it
self.executor.plan.insert_stage(next_stage)
return self.executor.stage_ok()
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
self.request.session[SESSION_KEY_STAGES] = stages
self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
return super().get(self.request, *args, **kwargs)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
res = super().post(request, *args, **kwargs)
if (
SESSION_KEY_SELECTED_STAGE in self.request.session
PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
):
self.logger.debug("Got selected stage in session, running that")
stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE)
self.logger.debug("Got selected stage in context, running that")
stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE)
# Because the foreign key to stage.configuration_stage points to
# a base stage class, we need to do another lookup
stage = Stage.objects.get_subclass(pk=stage_pk)
@ -291,8 +292,8 @@ class AuthenticatorValidateStageView(ChallengeStageView):
return res
def get_challenge(self) -> AuthenticatorValidationChallenge:
challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, [])
stages = self.request.session.get(SESSION_KEY_STAGES, [])
challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, [])
stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
stage_challenges = []
for stage in stages:
serializer = SelectableStageSerializer(
@ -307,6 +308,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
stage_challenges.append(serializer.data)
return AuthenticatorValidationChallenge(
data={
"component": "ak-stage-authenticator-validate",
"type": ChallengeTypes.NATIVE.value,
"device_challenges": challenges,
"configuration_stages": stage_challenges,
@ -317,7 +319,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
def cookie_jwt_key(self) -> str:
"""Signing key for MFA Cookie for this stage"""
return sha256(
f"{get_install_id()}:{self.executor.current_stage.pk.hex}".encode("ascii")
f"{settings.SECRET_KEY}:{self.executor.current_stage.pk.hex}".encode("ascii")
).hexdigest()
def check_mfa_cookie(self, allowed_devices: list[Device]):
@ -386,8 +388,3 @@ class AuthenticatorValidateStageView(ChallengeStageView):
"device": webauthn_device,
}
return self.set_valid_mfa_cookie(response.device)
def cleanup(self):
self.request.session.pop(SESSION_KEY_STAGES, None)
self.request.session.pop(SESSION_KEY_SELECTED_STAGE, None)
self.request.session.pop(SESSION_KEY_DEVICE_CHALLENGES, None)

View File

@ -1,26 +1,19 @@
"""Test validator stage"""
from unittest.mock import MagicMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.test.client import RequestFactory
from django.urls.base import reverse
from rest_framework.exceptions import ValidationError
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
from authentik.flows.planner import FlowPlan
from authentik.flows.stage import StageView
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id, generate_key
from authentik.lib.tests.utils import dummy_get_response
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import (
SESSION_KEY_DEVICE_CHALLENGES,
AuthenticatorValidationChallengeResponse,
)
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
from authentik.stages.identification.models import IdentificationStage, UserFields
@ -86,12 +79,17 @@ class AuthenticatorValidateStageTests(FlowTestCase):
def test_validate_selected_challenge(self):
"""Test validate_selected_challenge"""
# Prepare request with session
request = self.request_factory.get("/")
flow = create_test_flow()
stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP],
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session[SESSION_KEY_DEVICE_CHALLENGES] = [
session = self.client.session
plan = FlowPlan(flow_pk=flow.pk.hex)
plan.append_stage(stage)
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
{
"device_class": "static",
"device_uid": "1",
@ -101,23 +99,43 @@ class AuthenticatorValidateStageTests(FlowTestCase):
"device_uid": "2",
},
]
request.session.save()
session[SESSION_KEY_PLAN] = plan
session.save()
res = AuthenticatorValidationChallengeResponse()
res.stage = StageView(FlowExecutorView())
res.stage.request = request
with self.assertRaises(ValidationError):
res.validate_selected_challenge(
{
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
data={
"selected_challenge": {
"device_class": "baz",
"device_uid": "quox",
"challenge": {},
}
)
res.validate_selected_challenge(
{
"device_class": "static",
"device_uid": "1",
}
},
)
self.assertStageResponse(
response,
flow,
response_errors={
"selected_challenge": [{"string": "invalid challenge selected", "code": "invalid"}]
},
component="ak-stage-authenticator-validate",
)
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
data={
"selected_challenge": {
"device_class": "static",
"device_uid": "1",
"challenge": {},
},
},
)
self.assertStageResponse(
response,
flow,
response_errors={"non_field_errors": [{"string": "Empty response", "code": "invalid"}]},
component="ak-stage-authenticator-validate",
)
@patch(

View File

@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from hashlib import sha256
from time import sleep
from django.conf import settings
from django.test.client import RequestFactory
from django.urls.base import reverse
from django_otp.oath import TOTP
@ -16,7 +17,6 @@ from authentik.flows.stage import StageView
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import FlowExecutorView
from authentik.lib.generators import generate_id
from authentik.root.install_id import get_install_id
from authentik.stages.authenticator_validate.challenge import (
get_challenge_for_device,
validate_challenge_code,
@ -194,7 +194,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
"stage": stage.pk.hex + generate_id(),
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
},
key=sha256(f"{get_install_id()}:{stage.pk.hex}".encode("ascii")).hexdigest(),
key=sha256(f"{settings.SECRET_KEY}:{stage.pk.hex}".encode("ascii")).hexdigest(),
)
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
@ -233,7 +233,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
"stage": stage.pk.hex,
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
},
key=sha256(f"{get_install_id()}:{stage.pk.hex}".encode("ascii")).hexdigest(),
key=sha256(f"{settings.SECRET_KEY}:{stage.pk.hex}".encode("ascii")).hexdigest(),
)
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
@ -272,7 +272,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
"stage": stage.pk.hex,
"exp": (datetime.now() - timedelta(days=3)).timestamp(),
},
key=sha256(f"{get_install_id()}:{stage.pk.hex}".encode("ascii")).hexdigest(),
key=sha256(f"{settings.SECRET_KEY}:{stage.pk.hex}".encode("ascii")).hexdigest(),
)
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),

View File

@ -22,7 +22,7 @@ from authentik.stages.authenticator_validate.challenge import (
)
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import (
SESSION_KEY_DEVICE_CHALLENGES,
PLAN_CONTEXT_DEVICE_CHALLENGES,
AuthenticatorValidateStageView,
)
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
@ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
plan.append_stage(stage)
plan.append_stage(UserLoginStage(name=generate_id()))
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_DEVICE_CHALLENGES] = [
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
{
"device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk,
"challenge": {},
}
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
)
@ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
plan = FlowPlan(flow_pk=flow.pk.hex)
plan.append_stage(stage)
plan.append_stage(UserLoginStage(name=generate_id()))
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_DEVICE_CHALLENGES] = [
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
{
"device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk,
"challenge": {},
}
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
)

View File

@ -12,7 +12,7 @@ from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.models import FlowToken
from authentik.flows.models import FlowDesignation, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN
@ -82,6 +82,11 @@ class EmailStageView(ChallengeStageView):
"""Helper function that sends the actual email. Implies that you've
already checked that there is a pending user."""
pending_user = self.get_pending_user()
if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
# Pending user does not have a primary key, and we're in a recovery flow,
# which means the user entered an invalid identifier, so we pretend to send the
# email, to not disclose if the user exists
return
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
if not email:
email = pending_user.email

View File

@ -5,18 +5,20 @@ from unittest.mock import MagicMock, PropertyMock, patch
from django.core import mail
from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.stages.email.models import EmailStage
class TestEmailStageSending(APITestCase):
class TestEmailStageSending(FlowTestCase):
"""Email tests"""
def setUp(self):
@ -44,6 +46,13 @@ class TestEmailStageSending(APITestCase):
):
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")
events = Event.objects.filter(action=EventAction.EMAIL_SENT)
@ -54,6 +63,32 @@ class TestEmailStageSending(APITestCase):
self.assertEqual(event.context["to_email"], [self.user.email])
self.assertEqual(event.context["from_email"], "system@authentik.local")
def test_pending_fake_user(self):
"""Test with pending (fake) user"""
self.flow.designation = FlowDesignation.RECOVERY
self.flow.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = User(username=generate_id())
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
):
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 0)
def test_send_error(self):
"""Test error during sending (sending will be retried)"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

View File

@ -118,8 +118,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
username=uid_field,
email=uid_field,
)
self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
if not current_stage.show_matched_user:
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
if self.stage.executor.flow.designation == FlowDesignation.RECOVERY:
# When used in a recovery flow, always continue to not disclose if a user exists
return attrs
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
if not current_stage.password_stage:

View File

@ -188,7 +188,7 @@ class TestIdentificationStage(FlowTestCase):
],
)
def test_recovery_flow(self):
def test_link_recovery_flow(self):
"""Test that recovery flow is linked correctly"""
flow = create_test_flow()
self.stage.recovery_flow = flow
@ -226,6 +226,38 @@ class TestIdentificationStage(FlowTestCase):
],
)
def test_recovery_flow_invalid_user(self):
"""Test that an invalid user can proceed in a recovery flow"""
self.flow.designation = FlowDesignation.RECOVERY
self.flow.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
self.assertStageResponse(
response,
self.flow,
component="ak-stage-identification",
user_fields=["email"],
password_fields=False,
show_source_labels=False,
primary_action="Continue",
sources=[
{
"challenge": {
"component": "xak-flow-redirect",
"to": "/source/oauth/login/test/",
"type": ChallengeTypes.REDIRECT.value,
},
"icon_url": "/static/authentik/sources/default.svg",
"name": "test",
}
],
)
form_data = {"uid_field": generate_id()}
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
response = self.client.post(url, form_data)
self.assertEqual(response.status_code, 200)
def test_api_validate(self):
"""Test API validation"""
self.assertTrue(

View File

@ -5254,15 +5254,10 @@
],
"title": "Binding type"
},
"verification_kp": {
"type": "integer",
"title": "Verification Certificate",
"description": "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
},
"signing_kp": {
"type": "integer",
"title": "Signing Keypair",
"description": "Keypair used to sign outgoing Responses going to the Identity Provider."
"description": "Keypair which is used to sign outgoing requests. Leave empty to disable signing."
},
"digest_algorithm": {
"type": "string",
@ -8228,11 +8223,6 @@
"type": "string",
"minLength": 1,
"title": "Path"
},
"password": {
"type": "string",
"minLength": 1,
"title": "Password"
}
},
"required": []

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.6}
restart: unless-stopped
command: server
environment:
@ -49,11 +49,8 @@ services:
ports:
- "${COMPOSE_PORT_HTTP:-9000}:9000"
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
depends_on:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.5.6}
restart: unless-stopped
command: worker
environment:
@ -76,9 +73,6 @@ services:
- ./custom-templates:/templates
env_file:
- .env
depends_on:
- postgresql
- redis
volumes:
database:

4
go.mod
View File

@ -25,8 +25,8 @@ require (
github.com/prometheus/client_golang v1.15.1
github.com/sirupsen/logrus v1.9.2
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.3
goauthentik.io/api/v3 v3.2023052.1
github.com/stretchr/testify v1.8.2
goauthentik.io/api/v3 v3.2023050.2
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.8.0
golang.org/x/sync v0.2.0

9
go.sum
View File

@ -211,6 +211,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -218,8 +219,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@ -240,8 +241,8 @@ go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvx
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
goauthentik.io/api/v3 v3.2023052.1 h1:Sp3sBfkdBJCVTAxZte5fvBU4s0IZm6bqqli8ZawiER4=
goauthentik.io/api/v3 v3.2023052.1/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U=
goauthentik.io/api/v3 v3.2023050.2 h1:EnwEaPM2qSFwfow0G/pTk9GHXmux0ldN77b+/gMeGTM=
goauthentik.io/api/v3 v3.2023050.2/go.mod h1:nYECml4jGbp/541hj8GcylKQG1gVBsKppHy4+7G8u4U=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=

View File

@ -38,13 +38,14 @@ type RedisConfig struct {
}
type ListenConfig struct {
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS"`
}
type PathsConfig struct {

View File

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

View File

@ -0,0 +1,44 @@
package web
import (
"net"
"net/http"
"github.com/gorilla/handlers"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/config"
)
// ProxyHeaders Set proxy headers like X-Forwarded-For and such, but only if the direct connection
// comes from a client that's in a list of trusted CIDRs
func ProxyHeaders() func(http.Handler) http.Handler {
nets := []*net.IPNet{}
for _, rn := range config.Get().Listen.TrustedProxyCIDRs {
_, cidr, err := net.ParseCIDR(rn)
if err != nil {
continue
}
nets = append(nets, cidr)
}
ph := handlers.ProxyHeaders
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
// remoteAddr will be nil if the IP cannot be parsed
remoteAddr := net.ParseIP(host)
for _, allowedCidr := range nets {
if remoteAddr != nil && allowedCidr.Contains(remoteAddr) {
log.WithField("remoteAddr", remoteAddr).WithField("cidr", allowedCidr.String()).Trace("Setting proxy headers")
ph(h).ServeHTTP(w, r)
return
}
}
}
// Request is not directly coming from a CIDR we "trust"
// so set XFF to the direct host IP
r.Header.Set("X-Forwarded-For", host)
h.ServeHTTP(w, r)
})
}
}

View File

@ -35,7 +35,7 @@ type WebServer struct {
func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
l := log.WithField("logger", "authentik.router")
mainHandler := mux.NewRouter()
mainHandler.Use(handlers.ProxyHeaders)
mainHandler.Use(web.ProxyHeaders())
mainHandler.Use(handlers.CompressHandler)
loggingHandler := mainHandler.NewRoute().Subrouter()
loggingHandler.Use(web.NewLoggingHandler(l, nil))

View File

@ -15,7 +15,6 @@ from authentik import get_full_version
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.reflection import get_env
from authentik.root.install_id import get_install_id_raw
from lifecycle.worker import DjangoUvicornWorker
if TYPE_CHECKING:
@ -149,7 +148,9 @@ if not CONFIG.y_bool("disable_startup_analytics", False):
),
},
headers={
"User-Agent": sha512(get_install_id_raw().encode("ascii")).hexdigest()[:16],
"User-Agent": sha512(str(CONFIG.y("secret_key")).encode("ascii")).hexdigest()[
:16
],
"Content-Type": "application/json",
},
timeout=5,

View File

@ -1,45 +0,0 @@
# flake8: noqa
from uuid import uuid4
from authentik.lib.config import CONFIG
from lifecycle.migrate import BaseMigration
SQL_STATEMENT = """BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS authentik_install_id (
id TEXT NOT NULL
);
COMMIT;"""
class Migration(BaseMigration):
def needs_migration(self) -> bool:
self.cur.execute(
"select * from information_schema.tables where table_name = 'authentik_install_id';"
)
return not bool(self.cur.rowcount)
def upgrade(self, migrate=False):
self.cur.execute(SQL_STATEMENT)
self.con.commit()
if migrate:
# If we already have migrations in the database, assume we're upgrading an existing install
# and set the install id to the secret key
self.cur.execute(
"INSERT INTO authentik_install_id (id) VALUES (%s)", (CONFIG.y("secret_key"),)
)
else:
# Otherwise assume a new install, generate an install ID based on a UUID
install_id = str(uuid4())
self.cur.execute("INSERT INTO authentik_install_id (id) VALUES (%s)", (install_id,))
self.con.commit()
def run(self):
self.cur.execute(
"select * from information_schema.tables where table_name = 'django_migrations';"
)
if not bool(self.cur.rowcount):
# No django_migrations table, so generate a new id
return self.upgrade(migrate=False)
self.cur.execute("select count(*) from django_migrations;")
migrations = self.cur.fetchone()[0]
return self.upgrade(migrate=migrations > 0)

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-21 21:59+0000\n"
"POT-Creation-Date: 2023-05-18 14:21+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -31,16 +31,12 @@ msgstr ""
msgid "Validation Error"
msgstr ""
#: authentik/blueprints/api.py:43
msgid "Blueprint file does not exist"
msgstr ""
#: authentik/blueprints/api.py:54
#: authentik/blueprints/api.py:53
#, python-format
msgid "Failed to validate blueprint: %(logs)s"
msgstr ""
#: authentik/blueprints/api.py:59
#: authentik/blueprints/api.py:58
msgid "Either path or content must be set."
msgstr ""
@ -325,105 +321,105 @@ msgstr ""
msgid "Certificate-Key Pairs"
msgstr ""
#: authentik/events/models.py:290
#: authentik/events/models.py:293
msgid "Event"
msgstr ""
#: authentik/events/models.py:291
#: authentik/events/models.py:294
msgid "Events"
msgstr ""
#: authentik/events/models.py:297
#: authentik/events/models.py:300
msgid "authentik inbuilt notifications"
msgstr ""
#: authentik/events/models.py:298
#: authentik/events/models.py:301
msgid "Generic Webhook"
msgstr ""
#: authentik/events/models.py:299
#: authentik/events/models.py:302
msgid "Slack Webhook (Slack/Discord)"
msgstr ""
#: authentik/events/models.py:300
#: authentik/events/models.py:303
msgid "Email"
msgstr ""
#: authentik/events/models.py:318
#: authentik/events/models.py:321
msgid ""
"Only send notification once, for example when sending a webhook into a chat "
"channel."
msgstr ""
#: authentik/events/models.py:383
#: authentik/events/models.py:386
msgid "Severity"
msgstr ""
#: authentik/events/models.py:388
#: authentik/events/models.py:391
msgid "Dispatched for user"
msgstr ""
#: authentik/events/models.py:397
#: authentik/events/models.py:400
msgid "Event user"
msgstr ""
#: authentik/events/models.py:484
#: authentik/events/models.py:487
msgid "Notification Transport"
msgstr ""
#: authentik/events/models.py:485
#: authentik/events/models.py:488
msgid "Notification Transports"
msgstr ""
#: authentik/events/models.py:491
#: authentik/events/models.py:494
msgid "Notice"
msgstr ""
#: authentik/events/models.py:492
#: authentik/events/models.py:495
msgid "Warning"
msgstr ""
#: authentik/events/models.py:493
#: authentik/events/models.py:496
msgid "Alert"
msgstr ""
#: authentik/events/models.py:518
#: authentik/events/models.py:521
msgid "Notification"
msgstr ""
#: authentik/events/models.py:519
#: authentik/events/models.py:522
msgid "Notifications"
msgstr ""
#: authentik/events/models.py:529
#: authentik/events/models.py:532
msgid ""
"Select which transports should be used to notify the user. If none are "
"selected, the notification will only be shown in the authentik UI."
msgstr ""
#: authentik/events/models.py:537
#: authentik/events/models.py:540
msgid "Controls which severity level the created notifications will have."
msgstr ""
#: authentik/events/models.py:542
#: authentik/events/models.py:545
msgid ""
"Define which group of users this notification should be sent and shown to. "
"If left empty, Notification won't ben sent."
msgstr ""
#: authentik/events/models.py:560
#: authentik/events/models.py:563
msgid "Notification Rule"
msgstr ""
#: authentik/events/models.py:561
#: authentik/events/models.py:564
msgid "Notification Rules"
msgstr ""
#: authentik/events/models.py:581
#: authentik/events/models.py:584
msgid "Webhook Mapping"
msgstr ""
#: authentik/events/models.py:582
#: authentik/events/models.py:585
msgid "Webhook Mappings"
msgstr ""
@ -1287,49 +1283,49 @@ msgid ""
"minutes=2;seconds=3)."
msgstr ""
#: authentik/providers/saml/models.py:99 authentik/sources/saml/models.py:150
#: authentik/providers/saml/models.py:99 authentik/sources/saml/models.py:139
msgid "SHA1"
msgstr ""
#: authentik/providers/saml/models.py:100 authentik/sources/saml/models.py:151
#: authentik/providers/saml/models.py:100 authentik/sources/saml/models.py:140
msgid "SHA256"
msgstr ""
#: authentik/providers/saml/models.py:101 authentik/sources/saml/models.py:152
#: authentik/providers/saml/models.py:101 authentik/sources/saml/models.py:141
msgid "SHA384"
msgstr ""
#: authentik/providers/saml/models.py:102 authentik/sources/saml/models.py:153
#: authentik/providers/saml/models.py:102 authentik/sources/saml/models.py:142
msgid "SHA512"
msgstr ""
#: authentik/providers/saml/models.py:109 authentik/sources/saml/models.py:160
#: authentik/providers/saml/models.py:109 authentik/sources/saml/models.py:149
msgid "RSA-SHA1"
msgstr ""
#: authentik/providers/saml/models.py:110 authentik/sources/saml/models.py:161
#: authentik/providers/saml/models.py:110 authentik/sources/saml/models.py:150
msgid "RSA-SHA256"
msgstr ""
#: authentik/providers/saml/models.py:111 authentik/sources/saml/models.py:162
#: authentik/providers/saml/models.py:111 authentik/sources/saml/models.py:151
msgid "RSA-SHA384"
msgstr ""
#: authentik/providers/saml/models.py:112 authentik/sources/saml/models.py:163
#: authentik/providers/saml/models.py:112 authentik/sources/saml/models.py:152
msgid "RSA-SHA512"
msgstr ""
#: authentik/providers/saml/models.py:113 authentik/sources/saml/models.py:164
#: authentik/providers/saml/models.py:113 authentik/sources/saml/models.py:153
msgid "DSA-SHA1"
msgstr ""
#: authentik/providers/saml/models.py:124 authentik/sources/saml/models.py:130
#: authentik/providers/saml/models.py:124
msgid ""
"When selected, incoming assertion's Signatures will be validated against "
"this certificate. To allow unsigned Requests, leave on default."
msgstr ""
#: authentik/providers/saml/models.py:128 authentik/sources/saml/models.py:134
#: authentik/providers/saml/models.py:128
msgid "Verification Certificate"
msgstr ""
@ -1337,7 +1333,7 @@ msgstr ""
msgid "Keypair used to sign outgoing Responses going to the Service Provider."
msgstr ""
#: authentik/providers/saml/models.py:138 authentik/sources/saml/models.py:144
#: authentik/providers/saml/models.py:138 authentik/sources/saml/models.py:129
msgid "Signing Keypair"
msgstr ""
@ -1502,7 +1498,7 @@ msgstr ""
msgid "LDAP Property Mappings"
msgstr ""
#: authentik/sources/ldap/signals.py:59
#: authentik/sources/ldap/signals.py:56
msgid "Password does not match Active Directory Complexity."
msgstr ""
@ -1768,23 +1764,25 @@ msgid ""
"manually. (Format: hours=1;minutes=2;seconds=3)."
msgstr ""
#: authentik/sources/saml/models.py:142
msgid "Keypair used to sign outgoing Responses going to the Identity Provider."
#: authentik/sources/saml/models.py:131
msgid ""
"Keypair which is used to sign outgoing requests. Leave empty to disable "
"signing."
msgstr ""
#: authentik/sources/saml/models.py:226
#: authentik/sources/saml/models.py:215
msgid "SAML Source"
msgstr ""
#: authentik/sources/saml/models.py:227
#: authentik/sources/saml/models.py:216
msgid "SAML Sources"
msgstr ""
#: authentik/sources/saml/models.py:242
#: authentik/sources/saml/models.py:231
msgid "User SAML Source Connection"
msgstr ""
#: authentik/sources/saml/models.py:243
#: authentik/sources/saml/models.py:232
msgid "User SAML Source Connections"
msgstr ""

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-21 21:59+0000\n"
"POT-Creation-Date: 2023-05-10 17:31+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2023\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -37,16 +37,12 @@ msgstr "通用 API 错误"
msgid "Validation Error"
msgstr "校验错误"
#: authentik/blueprints/api.py:43
msgid "Blueprint file does not exist"
msgstr "蓝图文件不存在"
#: authentik/blueprints/api.py:54
#: authentik/blueprints/api.py:53
#, python-format
msgid "Failed to validate blueprint: %(logs)s"
msgstr "验证蓝图失败:%(logs)s"
#: authentik/blueprints/api.py:59
#: authentik/blueprints/api.py:58
msgid "Either path or content must be set."
msgstr "必须设置路径或内容。"
@ -341,105 +337,105 @@ msgstr "证书密钥对"
msgid "Certificate-Key Pairs"
msgstr "证书密钥对"
#: authentik/events/models.py:290
#: authentik/events/models.py:293
msgid "Event"
msgstr "事件"
#: authentik/events/models.py:291
#: authentik/events/models.py:294
msgid "Events"
msgstr "事件"
#: authentik/events/models.py:297
#: authentik/events/models.py:300
msgid "authentik inbuilt notifications"
msgstr "authentik 内置通知"
#: authentik/events/models.py:298
#: authentik/events/models.py:301
msgid "Generic Webhook"
msgstr "通用 Webhook"
#: authentik/events/models.py:299
#: authentik/events/models.py:302
msgid "Slack Webhook (Slack/Discord)"
msgstr "Slack WebhookSlack/Discord"
#: authentik/events/models.py:300
#: authentik/events/models.py:303
msgid "Email"
msgstr "电子邮箱"
#: authentik/events/models.py:318
#: authentik/events/models.py:321
msgid ""
"Only send notification once, for example when sending a webhook into a chat "
"channel."
msgstr "仅发送一次通知,例如在向聊天频道发送 Webhook 时。"
#: authentik/events/models.py:383
#: authentik/events/models.py:386
msgid "Severity"
msgstr "严重程度"
#: authentik/events/models.py:388
#: authentik/events/models.py:391
msgid "Dispatched for user"
msgstr "为用户分派"
#: authentik/events/models.py:397
#: authentik/events/models.py:400
msgid "Event user"
msgstr "事件用户"
#: authentik/events/models.py:484
#: authentik/events/models.py:487
msgid "Notification Transport"
msgstr "通知传输"
#: authentik/events/models.py:485
#: authentik/events/models.py:488
msgid "Notification Transports"
msgstr "通知传输"
#: authentik/events/models.py:491
#: authentik/events/models.py:494
msgid "Notice"
msgstr "通知"
#: authentik/events/models.py:492
#: authentik/events/models.py:495
msgid "Warning"
msgstr "警告"
#: authentik/events/models.py:493
#: authentik/events/models.py:496
msgid "Alert"
msgstr "注意"
#: authentik/events/models.py:518
#: authentik/events/models.py:521
msgid "Notification"
msgstr "通知"
#: authentik/events/models.py:519
#: authentik/events/models.py:522
msgid "Notifications"
msgstr "通知"
#: authentik/events/models.py:529
#: authentik/events/models.py:532
msgid ""
"Select which transports should be used to notify the user. If none are "
"selected, the notification will only be shown in the authentik UI."
msgstr "选择应使用哪些传输方式来通知用户。如果未选择任何内容,则通知将仅显示在 authentik UI 中。"
#: authentik/events/models.py:537
#: authentik/events/models.py:540
msgid "Controls which severity level the created notifications will have."
msgstr "控制被创建的通知的严重性级别。"
#: authentik/events/models.py:542
#: authentik/events/models.py:545
msgid ""
"Define which group of users this notification should be sent and shown to. "
"If left empty, Notification won't ben sent."
msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。"
#: authentik/events/models.py:560
#: authentik/events/models.py:563
msgid "Notification Rule"
msgstr "通知规则"
#: authentik/events/models.py:561
#: authentik/events/models.py:564
msgid "Notification Rules"
msgstr "通知规则"
#: authentik/events/models.py:581
#: authentik/events/models.py:584
msgid "Webhook Mapping"
msgstr "Webhook 映射"
#: authentik/events/models.py:582
#: authentik/events/models.py:585
msgid "Webhook Mappings"
msgstr "Webhook 映射"
@ -1316,49 +1312,49 @@ msgid ""
"hours=1;minutes=2;seconds=3)."
msgstr "从当前时间经过多久时或之后会话无效格式hours=1;minutes=2;seconds=3。"
#: authentik/providers/saml/models.py:99 authentik/sources/saml/models.py:150
#: authentik/providers/saml/models.py:99 authentik/sources/saml/models.py:139
msgid "SHA1"
msgstr "SHA1"
#: authentik/providers/saml/models.py:100 authentik/sources/saml/models.py:151
#: authentik/providers/saml/models.py:100 authentik/sources/saml/models.py:140
msgid "SHA256"
msgstr "SHA256"
#: authentik/providers/saml/models.py:101 authentik/sources/saml/models.py:152
#: authentik/providers/saml/models.py:101 authentik/sources/saml/models.py:141
msgid "SHA384"
msgstr "SHA384"
#: authentik/providers/saml/models.py:102 authentik/sources/saml/models.py:153
#: authentik/providers/saml/models.py:102 authentik/sources/saml/models.py:142
msgid "SHA512"
msgstr "SHA512"
#: authentik/providers/saml/models.py:109 authentik/sources/saml/models.py:160
#: authentik/providers/saml/models.py:109 authentik/sources/saml/models.py:149
msgid "RSA-SHA1"
msgstr "RSA-SHA1"
#: authentik/providers/saml/models.py:110 authentik/sources/saml/models.py:161
#: authentik/providers/saml/models.py:110 authentik/sources/saml/models.py:150
msgid "RSA-SHA256"
msgstr "RSA-SHA256"
#: authentik/providers/saml/models.py:111 authentik/sources/saml/models.py:162
#: authentik/providers/saml/models.py:111 authentik/sources/saml/models.py:151
msgid "RSA-SHA384"
msgstr "RSA-SHA384"
#: authentik/providers/saml/models.py:112 authentik/sources/saml/models.py:163
#: authentik/providers/saml/models.py:112 authentik/sources/saml/models.py:152
msgid "RSA-SHA512"
msgstr "RSA-SHA512"
#: authentik/providers/saml/models.py:113 authentik/sources/saml/models.py:164
#: authentik/providers/saml/models.py:113 authentik/sources/saml/models.py:153
msgid "DSA-SHA1"
msgstr "DSA-SHA1"
#: authentik/providers/saml/models.py:124 authentik/sources/saml/models.py:130
#: authentik/providers/saml/models.py:124
msgid ""
"When selected, incoming assertion's Signatures will be validated against "
"this certificate. To allow unsigned Requests, leave on default."
msgstr "选中后,传入断言的签名将根据此证书进行验证。要允许未签名的请求,请保留默认值。"
#: authentik/providers/saml/models.py:128 authentik/sources/saml/models.py:134
#: authentik/providers/saml/models.py:128
msgid "Verification Certificate"
msgstr "验证证书"
@ -1366,7 +1362,7 @@ msgstr "验证证书"
msgid "Keypair used to sign outgoing Responses going to the Service Provider."
msgstr "密钥对,用于签署发送给服务提供程序的传出响应。"
#: authentik/providers/saml/models.py:138 authentik/sources/saml/models.py:144
#: authentik/providers/saml/models.py:138 authentik/sources/saml/models.py:129
msgid "Signing Keypair"
msgstr "签名密钥对"
@ -1414,34 +1410,34 @@ msgstr "SCIM 映射"
msgid "SCIM Mappings"
msgstr "SCIM 映射"
#: authentik/providers/scim/tasks.py:52
#: authentik/providers/scim/tasks.py:50
msgid "Starting full SCIM sync"
msgstr "开始全量 SCIM 同步"
#: authentik/providers/scim/tasks.py:59
#: authentik/providers/scim/tasks.py:57
#, python-format
msgid "Syncing page %(page)d of users"
msgstr "正在同步用户页面 %(page)d"
#: authentik/providers/scim/tasks.py:63
#: authentik/providers/scim/tasks.py:61
#, python-format
msgid "Syncing page %(page)d of groups"
msgstr "正在同步群组页面 %(page)d"
#: authentik/providers/scim/tasks.py:92
#: authentik/providers/scim/tasks.py:90
#, python-format
msgid "Failed to sync user %(user_name)s due to remote error: %(error)s"
msgstr "由于远端错误,同步用户 %(user_name)s 失败:%(error)s"
msgid "Failed to sync user due to remote error %(name)s: %(error)s"
msgstr "由于远端错误 %(name)s,同步用户失败:%(error)s"
#: authentik/providers/scim/tasks.py:103 authentik/providers/scim/tasks.py:144
#: authentik/providers/scim/tasks.py:101 authentik/providers/scim/tasks.py:142
#, python-format
msgid "Stopping sync due to error: %(error)s"
msgstr "由于以下错误,同步停止:%(error)s"
#: authentik/providers/scim/tasks.py:133
#: authentik/providers/scim/tasks.py:131
#, python-format
msgid "Failed to sync group %(group_name)s due to remote error: %(error)s"
msgstr "由于远端错误,同步组 %(group_name)s 失败:%(error)s"
msgid "Failed to sync group due to remote error %(name)s: %(error)s"
msgstr "由于远端错误 %(name)s,同步群组失败:%(error)s"
#: authentik/recovery/management/commands/create_admin_group.py:11
msgid "Create admin group if the default group gets deleted."
@ -1531,7 +1527,7 @@ msgstr "LDAP 属性映射"
msgid "LDAP Property Mappings"
msgstr "LDAP 属性映射"
#: authentik/sources/ldap/signals.py:59
#: authentik/sources/ldap/signals.py:56
msgid "Password does not match Active Directory Complexity."
msgstr "密码与 Active Directory 复杂度不匹配。"
@ -1801,24 +1797,25 @@ msgstr ""
"删除临时用户的时间偏移。这仅适用于您的 IDP 使用 NameID 格式 'transient' "
"且用户未手动登出的情况。格式hours=1;minutes=2;seconds=3。"
#: authentik/sources/saml/models.py:142
#: authentik/sources/saml/models.py:131
msgid ""
"Keypair used to sign outgoing Responses going to the Identity Provider."
msgstr "密钥对,用于签署发送给身份提供程序的传出响应。"
"Keypair which is used to sign outgoing requests. Leave empty to disable "
"signing."
msgstr "用于签署传出请求的密钥对。留空则禁用签名。"
#: authentik/sources/saml/models.py:226
#: authentik/sources/saml/models.py:215
msgid "SAML Source"
msgstr "SAML 源"
#: authentik/sources/saml/models.py:227
#: authentik/sources/saml/models.py:216
msgid "SAML Sources"
msgstr "SAML 源"
#: authentik/sources/saml/models.py:242
#: authentik/sources/saml/models.py:231
msgid "User SAML Source Connection"
msgstr "用户 SAML 源连接"
#: authentik/sources/saml/models.py:243
#: authentik/sources/saml/models.py:232
msgid "User SAML Source Connections"
msgstr "用户 SAML 源连接"
@ -2153,10 +2150,6 @@ msgstr ""
" 这是一封测试电子邮件,用于通知您已成功配置 authentik 电子邮件。\n"
" "
#: authentik/stages/identification/api.py:20
msgid "When no user fields are selected, at least one source must be selected"
msgstr "如果未选择用户字段,则至少要选择一个源"
#: authentik/stages/identification/models.py:29
msgid ""
"Fields of the user object to match against. (Hold shift to select multiple "
@ -2450,18 +2443,17 @@ msgstr "用户写入阶段"
msgid "User Write Stages"
msgstr "用户写入阶段"
#: authentik/stages/user_write/stage.py:133
#: authentik/stages/user_write/stage.py:132
msgid "No Pending data."
msgstr "没有待处理的数据。"
#: authentik/stages/user_write/stage.py:139
#: authentik/stages/user_write/stage.py:138
msgid "No user found and can't create new user."
msgstr "未找到用户并且无法创建新用户。"
#: authentik/stages/user_write/stage.py:156
#: authentik/stages/user_write/stage.py:170
msgid "Failed to update user. Please try again later."
msgstr "更新用户失败。请稍后重试。"
#: authentik/stages/user_write/stage.py:165
msgid "Failed to save user"
msgstr "保存用户失败"
#: authentik/tenants/models.py:23
msgid ""

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-21 21:59+0000\n"
"POT-Creation-Date: 2023-05-10 17:31+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2023\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -37,16 +37,12 @@ msgstr "通用 API 错误"
msgid "Validation Error"
msgstr "校验错误"
#: authentik/blueprints/api.py:43
msgid "Blueprint file does not exist"
msgstr "蓝图文件不存在"
#: authentik/blueprints/api.py:54
#: authentik/blueprints/api.py:53
#, python-format
msgid "Failed to validate blueprint: %(logs)s"
msgstr "验证蓝图失败:%(logs)s"
#: authentik/blueprints/api.py:59
#: authentik/blueprints/api.py:58
msgid "Either path or content must be set."
msgstr "必须设置路径或内容。"
@ -341,105 +337,105 @@ msgstr "证书密钥对"
msgid "Certificate-Key Pairs"
msgstr "证书密钥对"
#: authentik/events/models.py:290
#: authentik/events/models.py:293
msgid "Event"
msgstr "事件"
#: authentik/events/models.py:291
#: authentik/events/models.py:294
msgid "Events"
msgstr "事件"
#: authentik/events/models.py:297
#: authentik/events/models.py:300
msgid "authentik inbuilt notifications"
msgstr "authentik 内置通知"
#: authentik/events/models.py:298
#: authentik/events/models.py:301
msgid "Generic Webhook"
msgstr "通用 Webhook"
#: authentik/events/models.py:299
#: authentik/events/models.py:302
msgid "Slack Webhook (Slack/Discord)"
msgstr "Slack WebhookSlack/Discord"
#: authentik/events/models.py:300
#: authentik/events/models.py:303
msgid "Email"
msgstr "电子邮箱"
#: authentik/events/models.py:318
#: authentik/events/models.py:321
msgid ""
"Only send notification once, for example when sending a webhook into a chat "
"channel."
msgstr "仅发送一次通知,例如在向聊天频道发送 Webhook 时。"
#: authentik/events/models.py:383
#: authentik/events/models.py:386
msgid "Severity"
msgstr "严重程度"
#: authentik/events/models.py:388
#: authentik/events/models.py:391
msgid "Dispatched for user"
msgstr "为用户分派"
#: authentik/events/models.py:397
#: authentik/events/models.py:400
msgid "Event user"
msgstr "事件用户"
#: authentik/events/models.py:484
#: authentik/events/models.py:487
msgid "Notification Transport"
msgstr "通知传输"
#: authentik/events/models.py:485
#: authentik/events/models.py:488
msgid "Notification Transports"
msgstr "通知传输"
#: authentik/events/models.py:491
#: authentik/events/models.py:494
msgid "Notice"
msgstr "通知"
#: authentik/events/models.py:492
#: authentik/events/models.py:495
msgid "Warning"
msgstr "警告"
#: authentik/events/models.py:493
#: authentik/events/models.py:496
msgid "Alert"
msgstr "注意"
#: authentik/events/models.py:518
#: authentik/events/models.py:521
msgid "Notification"
msgstr "通知"
#: authentik/events/models.py:519
#: authentik/events/models.py:522
msgid "Notifications"
msgstr "通知"
#: authentik/events/models.py:529
#: authentik/events/models.py:532
msgid ""
"Select which transports should be used to notify the user. If none are "
"selected, the notification will only be shown in the authentik UI."
msgstr "选择应使用哪些传输方式来通知用户。如果未选择任何内容,则通知将仅显示在 authentik UI 中。"
#: authentik/events/models.py:537
#: authentik/events/models.py:540
msgid "Controls which severity level the created notifications will have."
msgstr "控制被创建的通知的严重性级别。"
#: authentik/events/models.py:542
#: authentik/events/models.py:545
msgid ""
"Define which group of users this notification should be sent and shown to. "
"If left empty, Notification won't ben sent."
msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。"
#: authentik/events/models.py:560
#: authentik/events/models.py:563
msgid "Notification Rule"
msgstr "通知规则"
#: authentik/events/models.py:561
#: authentik/events/models.py:564
msgid "Notification Rules"
msgstr "通知规则"
#: authentik/events/models.py:581
#: authentik/events/models.py:584
msgid "Webhook Mapping"
msgstr "Webhook 映射"
#: authentik/events/models.py:582
#: authentik/events/models.py:585
msgid "Webhook Mappings"
msgstr "Webhook 映射"
@ -1316,49 +1312,49 @@ msgid ""
"hours=1;minutes=2;seconds=3)."
msgstr "从当前时间经过多久时或之后会话无效格式hours=1;minutes=2;seconds=3。"
#: authentik/providers/saml/models.py:99 authentik/sources/saml/models.py:150
#: authentik/providers/saml/models.py:99 authentik/sources/saml/models.py:139
msgid "SHA1"
msgstr "SHA1"
#: authentik/providers/saml/models.py:100 authentik/sources/saml/models.py:151
#: authentik/providers/saml/models.py:100 authentik/sources/saml/models.py:140
msgid "SHA256"
msgstr "SHA256"
#: authentik/providers/saml/models.py:101 authentik/sources/saml/models.py:152
#: authentik/providers/saml/models.py:101 authentik/sources/saml/models.py:141
msgid "SHA384"
msgstr "SHA384"
#: authentik/providers/saml/models.py:102 authentik/sources/saml/models.py:153
#: authentik/providers/saml/models.py:102 authentik/sources/saml/models.py:142
msgid "SHA512"
msgstr "SHA512"
#: authentik/providers/saml/models.py:109 authentik/sources/saml/models.py:160
#: authentik/providers/saml/models.py:109 authentik/sources/saml/models.py:149
msgid "RSA-SHA1"
msgstr "RSA-SHA1"
#: authentik/providers/saml/models.py:110 authentik/sources/saml/models.py:161
#: authentik/providers/saml/models.py:110 authentik/sources/saml/models.py:150
msgid "RSA-SHA256"
msgstr "RSA-SHA256"
#: authentik/providers/saml/models.py:111 authentik/sources/saml/models.py:162
#: authentik/providers/saml/models.py:111 authentik/sources/saml/models.py:151
msgid "RSA-SHA384"
msgstr "RSA-SHA384"
#: authentik/providers/saml/models.py:112 authentik/sources/saml/models.py:163
#: authentik/providers/saml/models.py:112 authentik/sources/saml/models.py:152
msgid "RSA-SHA512"
msgstr "RSA-SHA512"
#: authentik/providers/saml/models.py:113 authentik/sources/saml/models.py:164
#: authentik/providers/saml/models.py:113 authentik/sources/saml/models.py:153
msgid "DSA-SHA1"
msgstr "DSA-SHA1"
#: authentik/providers/saml/models.py:124 authentik/sources/saml/models.py:130
#: authentik/providers/saml/models.py:124
msgid ""
"When selected, incoming assertion's Signatures will be validated against "
"this certificate. To allow unsigned Requests, leave on default."
msgstr "选中后,传入断言的签名将根据此证书进行验证。要允许未签名的请求,请保留默认值。"
#: authentik/providers/saml/models.py:128 authentik/sources/saml/models.py:134
#: authentik/providers/saml/models.py:128
msgid "Verification Certificate"
msgstr "验证证书"
@ -1366,7 +1362,7 @@ msgstr "验证证书"
msgid "Keypair used to sign outgoing Responses going to the Service Provider."
msgstr "密钥对,用于签署发送给服务提供程序的传出响应。"
#: authentik/providers/saml/models.py:138 authentik/sources/saml/models.py:144
#: authentik/providers/saml/models.py:138 authentik/sources/saml/models.py:129
msgid "Signing Keypair"
msgstr "签名密钥对"
@ -1414,34 +1410,34 @@ msgstr "SCIM 映射"
msgid "SCIM Mappings"
msgstr "SCIM 映射"
#: authentik/providers/scim/tasks.py:52
#: authentik/providers/scim/tasks.py:50
msgid "Starting full SCIM sync"
msgstr "开始全量 SCIM 同步"
#: authentik/providers/scim/tasks.py:59
#: authentik/providers/scim/tasks.py:57
#, python-format
msgid "Syncing page %(page)d of users"
msgstr "正在同步用户页面 %(page)d"
#: authentik/providers/scim/tasks.py:63
#: authentik/providers/scim/tasks.py:61
#, python-format
msgid "Syncing page %(page)d of groups"
msgstr "正在同步群组页面 %(page)d"
#: authentik/providers/scim/tasks.py:92
#: authentik/providers/scim/tasks.py:90
#, python-format
msgid "Failed to sync user %(user_name)s due to remote error: %(error)s"
msgstr "由于远端错误,同步用户 %(user_name)s 失败:%(error)s"
msgid "Failed to sync user due to remote error %(name)s: %(error)s"
msgstr "由于远端错误 %(name)s,同步用户失败:%(error)s"
#: authentik/providers/scim/tasks.py:103 authentik/providers/scim/tasks.py:144
#: authentik/providers/scim/tasks.py:101 authentik/providers/scim/tasks.py:142
#, python-format
msgid "Stopping sync due to error: %(error)s"
msgstr "由于以下错误,同步停止:%(error)s"
#: authentik/providers/scim/tasks.py:133
#: authentik/providers/scim/tasks.py:131
#, python-format
msgid "Failed to sync group %(group_name)s due to remote error: %(error)s"
msgstr "由于远端错误,同步组 %(group_name)s 失败:%(error)s"
msgid "Failed to sync group due to remote error %(name)s: %(error)s"
msgstr "由于远端错误 %(name)s,同步群组失败:%(error)s"
#: authentik/recovery/management/commands/create_admin_group.py:11
msgid "Create admin group if the default group gets deleted."
@ -1531,7 +1527,7 @@ msgstr "LDAP 属性映射"
msgid "LDAP Property Mappings"
msgstr "LDAP 属性映射"
#: authentik/sources/ldap/signals.py:59
#: authentik/sources/ldap/signals.py:56
msgid "Password does not match Active Directory Complexity."
msgstr "密码与 Active Directory 复杂度不匹配。"
@ -1801,24 +1797,25 @@ msgstr ""
"删除临时用户的时间偏移。这仅适用于您的 IDP 使用 NameID 格式 'transient' "
"且用户未手动登出的情况。格式hours=1;minutes=2;seconds=3。"
#: authentik/sources/saml/models.py:142
#: authentik/sources/saml/models.py:131
msgid ""
"Keypair used to sign outgoing Responses going to the Identity Provider."
msgstr "密钥对,用于签署发送给身份提供程序的传出响应。"
"Keypair which is used to sign outgoing requests. Leave empty to disable "
"signing."
msgstr "用于签署传出请求的密钥对。留空则禁用签名。"
#: authentik/sources/saml/models.py:226
#: authentik/sources/saml/models.py:215
msgid "SAML Source"
msgstr "SAML 源"
#: authentik/sources/saml/models.py:227
#: authentik/sources/saml/models.py:216
msgid "SAML Sources"
msgstr "SAML 源"
#: authentik/sources/saml/models.py:242
#: authentik/sources/saml/models.py:231
msgid "User SAML Source Connection"
msgstr "用户 SAML 源连接"
#: authentik/sources/saml/models.py:243
#: authentik/sources/saml/models.py:232
msgid "User SAML Source Connections"
msgstr "用户 SAML 源连接"
@ -2153,10 +2150,6 @@ msgstr ""
" 这是一封测试电子邮件,用于通知您已成功配置 authentik 电子邮件。\n"
" "
#: authentik/stages/identification/api.py:20
msgid "When no user fields are selected, at least one source must be selected"
msgstr "如果未选择用户字段,则至少要选择一个源"
#: authentik/stages/identification/models.py:29
msgid ""
"Fields of the user object to match against. (Hold shift to select multiple "
@ -2450,18 +2443,17 @@ msgstr "用户写入阶段"
msgid "User Write Stages"
msgstr "用户写入阶段"
#: authentik/stages/user_write/stage.py:133
#: authentik/stages/user_write/stage.py:132
msgid "No Pending data."
msgstr "没有待处理的数据。"
#: authentik/stages/user_write/stage.py:139
#: authentik/stages/user_write/stage.py:138
msgid "No user found and can't create new user."
msgstr "未找到用户并且无法创建新用户。"
#: authentik/stages/user_write/stage.py:156
#: authentik/stages/user_write/stage.py:170
msgid "Failed to update user. Please try again later."
msgstr "更新用户失败。请稍后重试。"
#: authentik/stages/user_write/stage.py:165
msgid "Failed to save user"
msgstr "保存用户失败"
#: authentik/tenants/models.py:23
msgid ""

284
poetry.lock generated
View File

@ -878,72 +878,63 @@ files = [
[[package]]
name = "coverage"
version = "7.2.7"
version = "7.2.6"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
{file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
{file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
{file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
{file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
{file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
{file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
{file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
{file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
{file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
{file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
{file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
{file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
{file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
{file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
{file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
{file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
{file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
{file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
{file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
{file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
{file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
{file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
{file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
{file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
{file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
{file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
{file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
{file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
{file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
{file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
{file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
{file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
{file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
{file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
{file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
{file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
{file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
{file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
{file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
{file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
{file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
{file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
{file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
{file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
{file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
{file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
{file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
{file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
{file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
{file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
{file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
{file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
{file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
{file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
{file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
{file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
{file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
{file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
{file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
{file = "coverage-7.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:496b86f1fc9c81a1cd53d8842ef712e950a4611bba0c42d33366a7b91ba969ec"},
{file = "coverage-7.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbe6e8c0a9a7193ba10ee52977d4d5e7652957c1f56ccefed0701db8801a2a3b"},
{file = "coverage-7.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d06b721c2550c01a60e5d3093f417168658fb454e5dfd9a23570e9bffe39a1"},
{file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77a04b84d01f0e12c66f16e69e92616442dc675bbe51b90bfb074b1e5d1c7fbd"},
{file = "coverage-7.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35db06450272473eab4449e9c2ad9bc6a0a68dab8e81a0eae6b50d9c2838767e"},
{file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6727a0d929ff0028b1ed8b3e7f8701670b1d7032f219110b55476bb60c390bfb"},
{file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aac1d5fdc5378f6bac2c0c7ebe7635a6809f5b4376f6cf5d43243c1917a67087"},
{file = "coverage-7.2.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9e4a5eb1bbc3675ee57bc31f8eea4cd7fb0cbcbe4912cf1cb2bf3b754f4a80"},
{file = "coverage-7.2.6-cp310-cp310-win32.whl", hash = "sha256:71f739f97f5f80627f1fee2331e63261355fd1e9a9cce0016394b6707ac3f4ec"},
{file = "coverage-7.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:fde5c7a9d9864d3e07992f66767a9817f24324f354caa3d8129735a3dc74f126"},
{file = "coverage-7.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc7b667f8654376e9353dd93e55e12ce2a59fb6d8e29fce40de682273425e044"},
{file = "coverage-7.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:697f4742aa3f26c107ddcb2b1784a74fe40180014edbd9adaa574eac0529914c"},
{file = "coverage-7.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541280dde49ce74a4262c5e395b48ea1207e78454788887118c421cb4ffbfcac"},
{file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7f1a8328eeec34c54f1d5968a708b50fc38d31e62ca8b0560e84a968fbf9a9"},
{file = "coverage-7.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bbd58eb5a2371bf160590f4262109f66b6043b0b991930693134cb617bc0169"},
{file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae82c5f168d2a39a5d69a12a69d4dc23837a43cf2ca99be60dfe59996ea6b113"},
{file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f5440cdaf3099e7ab17a5a7065aed59aff8c8b079597b61c1f8be6f32fe60636"},
{file = "coverage-7.2.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6f03f87fea579d55e0b690d28f5042ec1368650466520fbc400e7aeaf09e995"},
{file = "coverage-7.2.6-cp311-cp311-win32.whl", hash = "sha256:dc4d5187ef4d53e0d4c8eaf530233685667844c5fb0b855fea71ae659017854b"},
{file = "coverage-7.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:c93d52c3dc7b9c65e39473704988602300e3cc1bad08b5ab5b03ca98bbbc68c1"},
{file = "coverage-7.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:42c692b55a647a832025a4c048007034fe77b162b566ad537ce65ad824b12a84"},
{file = "coverage-7.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7786b2fa7809bf835f830779ad285215a04da76293164bb6745796873f0942d"},
{file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25bad4196104761bc26b1dae9b57383826542ec689ff0042f7f4f4dd7a815cba"},
{file = "coverage-7.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2692306d3d4cb32d2cceed1e47cebd6b1d2565c993d6d2eda8e6e6adf53301e6"},
{file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:392154d09bd4473b9d11351ab5d63391f3d5d24d752f27b3be7498b0ee2b5226"},
{file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa079995432037b5e2ef5ddbb270bcd2ded9f52b8e191a5de11fe59a00ea30d8"},
{file = "coverage-7.2.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d712cefff15c712329113b01088ba71bbcef0f7ea58478ca0bbec63a824844cb"},
{file = "coverage-7.2.6-cp37-cp37m-win32.whl", hash = "sha256:004948e296149644d208964300cb3d98affc5211e9e490e9979af4030b0d6473"},
{file = "coverage-7.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c1d7a31603c3483ac49c1726723b0934f88f2c011c660e6471e7bd735c2fa110"},
{file = "coverage-7.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3436927d1794fa6763b89b60c896f9e3bd53212001026ebc9080d23f0c2733c1"},
{file = "coverage-7.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44c9b9f1a245f3d0d202b1a8fa666a80b5ecbe4ad5d0859c0fb16a52d9763224"},
{file = "coverage-7.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3783a286d5a93a2921396d50ce45a909aa8f13eee964465012f110f0cbb611"},
{file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cff6980fe7100242170092bb40d2b1cdad79502cd532fd26b12a2b8a5f9aee0"},
{file = "coverage-7.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c534431153caffc7c495c3eddf7e6a6033e7f81d78385b4e41611b51e8870446"},
{file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3062fd5c62df988cea9f2972c593f77fed1182bfddc5a3b12b1e606cb7aba99e"},
{file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6284a2005e4f8061c58c814b1600ad0074ccb0289fe61ea709655c5969877b70"},
{file = "coverage-7.2.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97729e6828643f168a2a3f07848e1b1b94a366b13a9f5aba5484c2215724edc8"},
{file = "coverage-7.2.6-cp38-cp38-win32.whl", hash = "sha256:dc11b42fa61ff1e788dd095726a0aed6aad9c03d5c5984b54cb9e1e67b276aa5"},
{file = "coverage-7.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:cbcc874f454ee51f158afd604a315f30c0e31dff1d5d5bf499fc529229d964dd"},
{file = "coverage-7.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d3cacc6a665221108ecdf90517a8028d07a2783df3417d12dcfef1c517e67478"},
{file = "coverage-7.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:272ab31228a9df857ab5df5d67936d8861464dc89c5d3fab35132626e9369379"},
{file = "coverage-7.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a8723ccec4e564d4b9a79923246f7b9a8de4ec55fa03ec4ec804459dade3c4f"},
{file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5906f6a84b47f995cd1bf0aca1c72d591c55ee955f98074e93660d64dfc66eb9"},
{file = "coverage-7.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c139b7ab3f0b15f9aad0a3fedef5a1f8c0b2bdc291d88639ca2c97d3682416"},
{file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a5ffd45c6b93c23a8507e2f436983015c6457aa832496b6a095505ca2f63e8f1"},
{file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4f3c7c19581d471af0e9cb49d928172cd8492cd78a2b7a4e82345d33662929bb"},
{file = "coverage-7.2.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e8c0e79820cdd67978e1120983786422d279e07a381dbf89d03bbb23ec670a6"},
{file = "coverage-7.2.6-cp39-cp39-win32.whl", hash = "sha256:13cde6bb0e58fb67d09e2f373de3899d1d1e866c5a9ff05d93615f2f54fbd2bb"},
{file = "coverage-7.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:6b9f64526286255735847aed0221b189486e0b9ed943446936e41b7e44b08783"},
{file = "coverage-7.2.6-pp37.pp38.pp39-none-any.whl", hash = "sha256:6babcbf1e66e46052442f10833cfc4a0d3554d8276aa37af8531a83ed3c1a01d"},
{file = "coverage-7.2.6.tar.gz", hash = "sha256:2025f913f2edb0272ef15d00b1f335ff8908c921c8eb2013536fcaf61f5a683d"},
]
[package.extras]
@ -1136,14 +1127,14 @@ Django = ">=3.2"
[[package]]
name = "django-otp"
version = "1.2.1"
version = "1.2.0"
description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "django_otp-1.2.1-py3-none-any.whl", hash = "sha256:1758aabfc17c30ba00142e823c961e20d2a0353040a9e4e4be2688cf71085446"},
{file = "django_otp-1.2.1.tar.gz", hash = "sha256:d4785291fc97eb61c02574660f7ae82baab04da187fa5d0fb649d57deea2c62e"},
{file = "django_otp-1.2.0-py3-none-any.whl", hash = "sha256:aa14ace751bede7c6c385f2ea3589f6aa3565a31e455fa0ee69801b79761e3b0"},
{file = "django_otp-1.2.0.tar.gz", hash = "sha256:2baa30237f46549446d8d17a790b962f9065168bad38968dd208cdeb85901ede"},
]
[package.dependencies]
@ -2271,14 +2262,14 @@ files = [
[[package]]
name = "paramiko"
version = "3.2.0"
version = "3.1.0"
description = "SSH2 protocol library"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "paramiko-3.2.0-py3-none-any.whl", hash = "sha256:df0f9dd8903bc50f2e10580af687f3015bf592a377cd438d2ec9546467a14eb8"},
{file = "paramiko-3.2.0.tar.gz", hash = "sha256:93cdce625a8a1dc12204439d45033f3261bdb2c201648cfcdc06f9fd0f94ec29"},
{file = "paramiko-3.1.0-py3-none-any.whl", hash = "sha256:f0caa660e797d9cd10db6fc6ae81e2c9b2767af75c3180fcd0e46158cd368d7f"},
{file = "paramiko-3.1.0.tar.gz", hash = "sha256:6950faca6819acd3219d4ae694a23c7a87ee38d084f70c1724b0c0dbb8b75769"},
]
[package.dependencies]
@ -2502,44 +2493,45 @@ files = [
[[package]]
name = "pycryptodome"
version = "3.18.0"
version = "3.17"
description = "Cryptographic library for Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "pycryptodome-3.18.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:d1497a8cd4728db0e0da3c304856cb37c0c4e3d0b36fcbabcc1600f18504fc54"},
{file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:928078c530da78ff08e10eb6cada6e0dff386bf3d9fa9871b4bbc9fbc1efe024"},
{file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:157c9b5ba5e21b375f052ca78152dd309a09ed04703fd3721dce3ff8ecced148"},
{file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:d20082bdac9218649f6abe0b885927be25a917e29ae0502eaf2b53f1233ce0c2"},
{file = "pycryptodome-3.18.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e8ad74044e5f5d2456c11ed4cfd3e34b8d4898c0cb201c4038fe41458a82ea27"},
{file = "pycryptodome-3.18.0-cp27-cp27m-win32.whl", hash = "sha256:62a1e8847fabb5213ccde38915563140a5b338f0d0a0d363f996b51e4a6165cf"},
{file = "pycryptodome-3.18.0-cp27-cp27m-win_amd64.whl", hash = "sha256:16bfd98dbe472c263ed2821284118d899c76968db1a6665ade0c46805e6b29a4"},
{file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7a3d22c8ee63de22336679e021c7f2386f7fc465477d59675caa0e5706387944"},
{file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:78d863476e6bad2a592645072cc489bb90320972115d8995bcfbee2f8b209918"},
{file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:b6a610f8bfe67eab980d6236fdc73bfcdae23c9ed5548192bb2d530e8a92780e"},
{file = "pycryptodome-3.18.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:422c89fd8df8a3bee09fb8d52aaa1e996120eafa565437392b781abec2a56e14"},
{file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:9ad6f09f670c466aac94a40798e0e8d1ef2aa04589c29faa5b9b97566611d1d1"},
{file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:53aee6be8b9b6da25ccd9028caf17dcdce3604f2c7862f5167777b707fbfb6cb"},
{file = "pycryptodome-3.18.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:10da29526a2a927c7d64b8f34592f461d92ae55fc97981aab5bbcde8cb465bb6"},
{file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f21efb8438971aa16924790e1c3dba3a33164eb4000106a55baaed522c261acf"},
{file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4944defabe2ace4803f99543445c27dd1edbe86d7d4edb87b256476a91e9ffa4"},
{file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:51eae079ddb9c5f10376b4131be9589a6554f6fd84f7f655180937f611cd99a2"},
{file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:83c75952dcf4a4cebaa850fa257d7a860644c70a7cd54262c237c9f2be26f76e"},
{file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:957b221d062d5752716923d14e0926f47670e95fead9d240fa4d4862214b9b2f"},
{file = "pycryptodome-3.18.0-cp35-abi3-win32.whl", hash = "sha256:795bd1e4258a2c689c0b1f13ce9684fa0dd4c0e08680dcf597cf9516ed6bc0f3"},
{file = "pycryptodome-3.18.0-cp35-abi3-win_amd64.whl", hash = "sha256:b1d9701d10303eec8d0bd33fa54d44e67b8be74ab449052a8372f12a66f93fb9"},
{file = "pycryptodome-3.18.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:cb1be4d5af7f355e7d41d36d8eec156ef1382a88638e8032215c215b82a4b8ec"},
{file = "pycryptodome-3.18.0-pp27-pypy_73-win32.whl", hash = "sha256:fc0a73f4db1e31d4a6d71b672a48f3af458f548059aa05e83022d5f61aac9c08"},
{file = "pycryptodome-3.18.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f022a4fd2a5263a5c483a2bb165f9cb27f2be06f2f477113783efe3fe2ad887b"},
{file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:363dd6f21f848301c2dcdeb3c8ae5f0dee2286a5e952a0f04954b82076f23825"},
{file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12600268763e6fec3cefe4c2dcdf79bde08d0b6dc1813887e789e495cb9f3403"},
{file = "pycryptodome-3.18.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4604816adebd4faf8810782f137f8426bf45fee97d8427fa8e1e49ea78a52e2c"},
{file = "pycryptodome-3.18.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:01489bbdf709d993f3058e2996f8f40fee3f0ea4d995002e5968965fa2fe89fb"},
{file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3811e31e1ac3069988f7a1c9ee7331b942e605dfc0f27330a9ea5997e965efb2"},
{file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4b967bb11baea9128ec88c3d02f55a3e338361f5e4934f5240afcb667fdaec"},
{file = "pycryptodome-3.18.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9c8eda4f260072f7dbe42f473906c659dcbadd5ae6159dfb49af4da1293ae380"},
{file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"},
{file = "pycryptodome-3.17-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:2c5631204ebcc7ae33d11c43037b2dafe25e2ab9c1de6448eb6502ac69c19a56"},
{file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:04779cc588ad8f13c80a060b0b1c9d1c203d051d8a43879117fe6b8aaf1cd3fa"},
{file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f812d58c5af06d939b2baccdda614a3ffd80531a26e5faca2c9f8b1770b2b7af"},
{file = "pycryptodome-3.17-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:9453b4e21e752df8737fdffac619e93c9f0ec55ead9a45df782055eb95ef37d9"},
{file = "pycryptodome-3.17-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:121d61663267f73692e8bde5ec0d23c9146465a0d75cad75c34f75c752527b01"},
{file = "pycryptodome-3.17-cp27-cp27m-win32.whl", hash = "sha256:ba2d4fcb844c6ba5df4bbfee9352ad5352c5ae939ac450e06cdceff653280450"},
{file = "pycryptodome-3.17-cp27-cp27m-win_amd64.whl", hash = "sha256:87e2ca3aa557781447428c4b6c8c937f10ff215202ab40ece5c13a82555c10d6"},
{file = "pycryptodome-3.17-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f44c0d28716d950135ff21505f2c764498eda9d8806b7c78764165848aa419bc"},
{file = "pycryptodome-3.17-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5a790bc045003d89d42e3b9cb3cc938c8561a57a88aaa5691512e8540d1ae79c"},
{file = "pycryptodome-3.17-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:d086d46774e27b280e4cece8ab3d87299cf0d39063f00f1e9290d096adc5662a"},
{file = "pycryptodome-3.17-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5587803d5b66dfd99e7caa31ed91fba0fdee3661c5d93684028ad6653fce725f"},
{file = "pycryptodome-3.17-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:e7debd9c439e7b84f53be3cf4ba8b75b3d0b6e6015212355d6daf44ac672e210"},
{file = "pycryptodome-3.17-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ca1ceb6303be1282148f04ac21cebeebdb4152590842159877778f9cf1634f09"},
{file = "pycryptodome-3.17-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:dc22cc00f804485a3c2a7e2010d9f14a705555f67020eb083e833cabd5bd82e4"},
{file = "pycryptodome-3.17-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80ea8333b6a5f2d9e856ff2293dba2e3e661197f90bf0f4d5a82a0a6bc83a626"},
{file = "pycryptodome-3.17-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c133f6721fba313722a018392a91e3c69d3706ae723484841752559e71d69dc6"},
{file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:333306eaea01fde50a73c4619e25631e56c4c61bd0fb0a2346479e67e3d3a820"},
{file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1a30f51b990994491cec2d7d237924e5b6bd0d445da9337d77de384ad7f254f9"},
{file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:909e36a43fe4a8a3163e9c7fc103867825d14a2ecb852a63d3905250b308a4e5"},
{file = "pycryptodome-3.17-cp35-abi3-win32.whl", hash = "sha256:a3228728a3808bc9f18c1797ec1179a0efb5068c817b2ffcf6bcd012494dffb2"},
{file = "pycryptodome-3.17-cp35-abi3-win_amd64.whl", hash = "sha256:9ec565e89a6b400eca814f28d78a9ef3f15aea1df74d95b28b7720739b28f37f"},
{file = "pycryptodome-3.17-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:e1819b67bcf6ca48341e9b03c2e45b1c891fa8eb1a8458482d14c2805c9616f2"},
{file = "pycryptodome-3.17-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:f8e550caf52472ae9126953415e4fc554ab53049a5691c45b8816895c632e4d7"},
{file = "pycryptodome-3.17-pp27-pypy_73-win32.whl", hash = "sha256:afbcdb0eda20a0e1d44e3a1ad6d4ec3c959210f4b48cabc0e387a282f4c7deb8"},
{file = "pycryptodome-3.17-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a74f45aee8c5cc4d533e585e0e596e9f78521e1543a302870a27b0ae2106381e"},
{file = "pycryptodome-3.17-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38bbd6717eac084408b4094174c0805bdbaba1f57fc250fd0309ae5ec9ed7e09"},
{file = "pycryptodome-3.17-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f68d6c8ea2974a571cacb7014dbaada21063a0375318d88ac1f9300bc81e93c3"},
{file = "pycryptodome-3.17-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8198f2b04c39d817b206ebe0db25a6653bb5f463c2319d6f6d9a80d012ac1e37"},
{file = "pycryptodome-3.17-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a232474cd89d3f51e4295abe248a8b95d0332d153bf46444e415409070aae1e"},
{file = "pycryptodome-3.17-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4992ec965606054e8326e83db1c8654f0549cdb26fce1898dc1a20bc7684ec1c"},
{file = "pycryptodome-3.17-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53068e33c74f3b93a8158dacaa5d0f82d254a81b1002e0cd342be89fcb3433eb"},
{file = "pycryptodome-3.17-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:74794a2e2896cd0cf56fdc9db61ef755fa812b4a4900fa46c49045663a92b8d0"},
{file = "pycryptodome-3.17.tar.gz", hash = "sha256:bce2e2d8e82fcf972005652371a3e8731956a0c1fbb719cc897943b3695ad91b"},
]
[[package]]
@ -3021,21 +3013,21 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
[[package]]
name = "requests"
version = "2.31.0"
version = "2.28.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.7, <4"
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
charset-normalizer = ">=2,<3"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
urllib3 = ">=1.21.1,<1.27"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
@ -3116,29 +3108,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.0.270"
version = "0.0.267"
description = "An extremely fast Python linter, written in Rust."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.0.270-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:f74c4d550f7b8e808455ac77bbce38daafc458434815ba0bc21ae4bdb276509b"},
{file = "ruff-0.0.270-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:643de865fd35cb76c4f0739aea5afe7b8e4d40d623df7e9e6ea99054e5cead0a"},
{file = "ruff-0.0.270-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca02e709b3308eb7255b5f74e779be23b5980fca3862eae28bb23069cd61ae4"},
{file = "ruff-0.0.270-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ed3b198768d2b3a2300fb18f730cd39948a5cc36ba29ae9d4639a11040880be"},
{file = "ruff-0.0.270-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:739495d2dbde87cf4e3110c8d27bc20febf93112539a968a4e02c26f0deccd1d"},
{file = "ruff-0.0.270-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:08188f8351f4c0b6216e8463df0a76eb57894ca59a3da65e4ed205db980fd3ae"},
{file = "ruff-0.0.270-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0827b074635d37984fc98d99316bfab5c8b1231bb83e60dacc83bd92883eedb4"},
{file = "ruff-0.0.270-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d61ae4841313f6eeb8292dc349bef27b4ce426e62c36e80ceedc3824e408734"},
{file = "ruff-0.0.270-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eb412f20e77529a01fb94d578b19dcb8331b56f93632aa0cce4a2ea27b7aeba"},
{file = "ruff-0.0.270-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b775e2c5fc869359daf8c8b8aa0fd67240201ab2e8d536d14a0edf279af18786"},
{file = "ruff-0.0.270-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:21f00e47ab2308617c44435c8dfd9e2e03897461c9e647ec942deb2a235b4cfd"},
{file = "ruff-0.0.270-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0bbfbf6fd2436165566ca85f6e57be03ed2f0a994faf40180cfbb3604c9232ef"},
{file = "ruff-0.0.270-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8af391ef81f7be960be10886a3c1aac0b298bde7cb9a86ec2b05faeb2081ce6b"},
{file = "ruff-0.0.270-py3-none-win32.whl", hash = "sha256:b4c037fe2f75bcd9aed0c89c7c507cb7fa59abae2bd4c8b6fc331a28178655a4"},
{file = "ruff-0.0.270-py3-none-win_amd64.whl", hash = "sha256:0012f9b7dc137ab7f1f0355e3c4ca49b562baf6c9fa1180948deeb6648c52957"},
{file = "ruff-0.0.270-py3-none-win_arm64.whl", hash = "sha256:9613456b0b375766244c25045e353bc8890c856431cd97893c97b10cc93bd28d"},
{file = "ruff-0.0.270.tar.gz", hash = "sha256:95db07b7850b30ebf32b27fe98bc39e0ab99db3985edbbf0754d399eb2f0e690"},
{file = "ruff-0.0.267-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:4adbbbe314d8fcc539a245065bad89446a3cef2e0c9cf70bf7bb9ed6fe31856d"},
{file = "ruff-0.0.267-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:67254ae34c38cba109fdc52e4a70887de1f850fb3971e5eeef343db67305d1c1"},
{file = "ruff-0.0.267-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbe104f21a429b77eb5ac276bd5352fd8c0e1fbb580b4c772f77ee8c76825654"},
{file = "ruff-0.0.267-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db33deef2a5e1cf528ca51cc59dd764122a48a19a6c776283b223d147041153f"},
{file = "ruff-0.0.267-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9adf1307fa9d840d1acaa477eb04f9702032a483214c409fca9dc46f5f157fe3"},
{file = "ruff-0.0.267-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0afca3633c8e2b6c0a48ad0061180b641b3b404d68d7e6736aab301c8024c424"},
{file = "ruff-0.0.267-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2972241065b1c911bce3db808837ed10f4f6f8a8e15520a4242d291083605ab6"},
{file = "ruff-0.0.267-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f731d81cb939e757b0335b0090f18ca2e9ff8bcc8e6a1cf909245958949b6e11"},
{file = "ruff-0.0.267-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20c594eb56c19063ef5a57f89340e64c6550e169d6a29408a45130a8c3068adc"},
{file = "ruff-0.0.267-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:45d61a2b01bdf61581a2ee039503a08aa603dc74a6bbe6fb5d1ce3052f5370e5"},
{file = "ruff-0.0.267-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2107cec3699ca4d7bd41543dc1d475c97ae3a21ea9212238b5c2088fa8ee7722"},
{file = "ruff-0.0.267-py3-none-musllinux_1_2_i686.whl", hash = "sha256:786de30723c71fc46b80a173c3313fc0dbe73c96bd9da8dd1212cbc2f84cdfb2"},
{file = "ruff-0.0.267-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a898953949e37c109dd242cfcf9841e065319995ebb7cdfd213b446094a942f"},
{file = "ruff-0.0.267-py3-none-win32.whl", hash = "sha256:d12ab329474c46b96d962e2bdb92e3ad2144981fe41b89c7770f370646c0101f"},
{file = "ruff-0.0.267-py3-none-win_amd64.whl", hash = "sha256:d09aecc9f5845586ba90911d815f9772c5a6dcf2e34be58c6017ecb124534ac4"},
{file = "ruff-0.0.267-py3-none-win_arm64.whl", hash = "sha256:7df7eb5f8d791566ba97cc0b144981b9c080a5b861abaf4bb35a26c8a77b83e9"},
{file = "ruff-0.0.267.tar.gz", hash = "sha256:632cec7bbaf3c06fcf0a72a1dd029b7d8b7f424ba95a574aaa135f5d20a00af7"},
]
[[package]]
@ -3161,14 +3153,14 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
[[package]]
name = "sentry-sdk"
version = "1.24.0"
version = "1.23.1"
description = "Python client for Sentry (https://sentry.io)"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "sentry-sdk-1.24.0.tar.gz", hash = "sha256:0bbcecda9f51936904c1030e7fef0fe693e633888f02a14d1cb68646a50e83b3"},
{file = "sentry_sdk-1.24.0-py2.py3-none-any.whl", hash = "sha256:56d6d9d194c898d853a7c1dd99bed92ce82334ee1282292c15bcc967ff1a49b5"},
{file = "sentry-sdk-1.23.1.tar.gz", hash = "sha256:0300fbe7a07b3865b3885929fb863a68ff01f59e3bcfb4e7953d0bf7fd19c67f"},
{file = "sentry_sdk-1.23.1-py2.py3-none-any.whl", hash = "sha256:a884e2478e0b055776ea2b9234d5de9339b4bae0b3a5e74ae43d131db8ded27e"},
]
[package.dependencies]
@ -3385,23 +3377,23 @@ files = [
[[package]]
name = "tornado"
version = "6.3.2"
version = "6.2"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
category = "main"
optional = false
python-versions = ">= 3.8"
python-versions = ">= 3.7"
files = [
{file = "tornado-6.3.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829"},
{file = "tornado-6.3.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c"},
{file = "tornado-6.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f"},
{file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4"},
{file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe"},
{file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d"},
{file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0"},
{file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411"},
{file = "tornado-6.3.2-cp38-abi3-win32.whl", hash = "sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2"},
{file = "tornado-6.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf"},
{file = "tornado-6.3.2.tar.gz", hash = "sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba"},
{file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"},
{file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"},
{file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"},
{file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"},
{file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"},
{file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"},
{file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"},
{file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"},
{file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"},
{file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"},
{file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"},
]
[[package]]
@ -3444,14 +3436,14 @@ wsproto = ">=0.14"
[[package]]
name = "twilio"
version = "8.2.1"
version = "8.2.0"
description = "Twilio API client and TwiML generator"
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "twilio-8.2.1-py2.py3-none-any.whl", hash = "sha256:9c6bbfda1f3196c64258b6661e372c23e2e6a9630c9986f725d16bb4bfe3275f"},
{file = "twilio-8.2.1.tar.gz", hash = "sha256:66fe6a18199955b8abce2699e533b56f605a22d585c3f0b1820113ec068a0b51"},
{file = "twilio-8.2.0-py2.py3-none-any.whl", hash = "sha256:23eceaec183995fc827e3bfad229cca6e1944bfd9604e57e2712e625b6e01223"},
{file = "twilio-8.2.0.tar.gz", hash = "sha256:0c19eb6a5b84dbcd15658e23a142df026297236e4d72ad9304fd95e7dbff2662"},
]
[package.dependencies]
@ -3592,14 +3584,14 @@ files = [
[[package]]
name = "urllib3"
version = "1.26.16"
version = "1.26.15"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
files = [
{file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"},
{file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"},
{file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"},
{file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"},
]
[package.dependencies]

View File

@ -113,7 +113,7 @@ filterwarnings = [
[tool.poetry]
name = "authentik"
version = "2023.5.2"
version = "2023.5.6"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2023.5.2
version: 2023.5.6
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -4783,6 +4783,38 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/{id}/impersonate/:
post:
operationId: core_users_impersonate_create
description: Impersonate a user
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this User.
required: true
tags:
- core
security:
- authentik: []
responses:
'204':
description: Successfully started impersonation
'401':
description: Access denied
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/{id}/metrics/:
get:
operationId: core_users_metrics_retrieve
@ -4962,6 +4994,29 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/impersonate_end/:
get:
operationId: core_users_impersonate_end_retrieve
description: End Impersonation a user
tags:
- core
security:
- authentik: []
responses:
'204':
description: Successfully started impersonation
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/users/me/:
get:
operationId: core_users_me_retrieve
@ -18450,11 +18505,6 @@ paths:
* `email_deny` - Use the user's email address, but deny enrollment when the email address already exists.
* `username_link` - Link to a user with identical username. Can have security implications when a username is used with another source.
* `username_deny` - Use the user's username, but deny enrollment when the username already exists.
- in: query
name: verification_kp
schema:
type: string
format: uuid
tags:
- sources
security:
@ -37285,20 +37335,13 @@ components:
* `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient
binding_type:
$ref: '#/components/schemas/BindingTypeEnum'
verification_kp:
type: string
format: uuid
nullable: true
title: Verification Certificate
description: When selected, incoming assertion's Signatures will be validated
against this certificate. To allow unsigned Requests, leave on default.
signing_kp:
type: string
format: uuid
nullable: true
title: Signing Keypair
description: Keypair used to sign outgoing Responses going to the Identity
Provider.
description: Keypair which is used to sign outgoing requests. Leave empty
to disable signing.
digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm:
@ -39620,20 +39663,13 @@ components:
* `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient
binding_type:
$ref: '#/components/schemas/BindingTypeEnum'
verification_kp:
type: string
format: uuid
nullable: true
title: Verification Certificate
description: When selected, incoming assertion's Signatures will be validated
against this certificate. To allow unsigned Requests, leave on default.
signing_kp:
type: string
format: uuid
nullable: true
title: Signing Keypair
description: Keypair used to sign outgoing Responses going to the Identity
Provider.
description: Keypair which is used to sign outgoing requests. Leave empty
to disable signing.
digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm:
@ -39734,20 +39770,13 @@ components:
* `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` - Transient
binding_type:
$ref: '#/components/schemas/BindingTypeEnum'
verification_kp:
type: string
format: uuid
nullable: true
title: Verification Certificate
description: When selected, incoming assertion's Signatures will be validated
against this certificate. To allow unsigned Requests, leave on default.
signing_kp:
type: string
format: uuid
nullable: true
title: Signing Keypair
description: Keypair used to sign outgoing Responses going to the Identity
Provider.
description: Keypair which is used to sign outgoing requests. Leave empty
to disable signing.
digest_algorithm:
$ref: '#/components/schemas/DigestAlgorithmEnum'
signature_algorithm:
@ -40519,12 +40548,6 @@ components:
type: object
description: Get system information.
properties:
env:
type: object
additionalProperties:
type: string
description: Get Environment
readOnly: true
http_headers:
type: object
additionalProperties:
@ -40578,7 +40601,6 @@ components:
readOnly: true
required:
- embedded_outpost_host
- env
- http_headers
- http_host
- http_is_secure

1893
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,15 +24,15 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.2.2",
"@fortawesome/fontawesome-free": "^6.4.0",
"@goauthentik/api": "^2023.5.2-1685273038",
"@lingui/cli": "^4.2.0",
"@lingui/core": "^4.2.0",
"@lingui/detect-locale": "^4.2.0",
"@lingui/format-po-gettext": "^4.2.0",
"@lingui/macro": "^4.2.0",
"@goauthentik/api": "^2023.5.3-1687462221",
"@lingui/cli": "^4.1.2",
"@lingui/core": "^4.1.2",
"@lingui/detect-locale": "^4.1.2",
"@lingui/format-po-gettext": "^4.1.2",
"@lingui/macro": "^4.1.2",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.53.1",
"@sentry/tracing": "^7.53.1",
"@sentry/browser": "^7.52.1",
"@sentry/tracing": "^7.52.1",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.3.0",
@ -43,18 +43,16 @@
"country-flag-icons": "^1.5.7",
"fuse.js": "^6.6.2",
"lit": "^2.7.4",
"mermaid": "^10.2.0",
"mermaid": "^10.1.0",
"rapidoc": "^9.3.4",
"webcomponent-qr-code": "^1.1.1",
"yaml": "^2.3.1"
"yaml": "^2.2.2"
},
"devDependencies": {
"@babel/core": "^7.22.1",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.22.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.22.4",
"@babel/preset-env": "^7.22.4",
"@babel/core": "^7.21.8",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/preset-env": "^7.21.5",
"@babel/preset-typescript": "^7.21.5",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
@ -68,23 +66,23 @@
"@types/chart.js": "^2.9.37",
"@types/codemirror": "5.60.7",
"@types/grecaptcha": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-tsconfig-paths": "^1.0.3",
"eslint": "^8.41.0",
"eslint": "^8.40.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.8.3",
"prettier": "^2.8.8",
"pyright": "^1.1.310",
"pyright": "^1.1.308",
"rollup": "^2.79.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-cssimport": "^1.0.3",
"rollup-plugin-minify-html-literals": "^1.2.6",
"rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.5.2",
"tslib": "^2.5.1",
"turnstile-types": "^1.1.2",
"typescript": "^5.0.4"
}

View File

@ -31,7 +31,7 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { AdminApi, SessionUser, Version } from "@goauthentik/api";
import { AdminApi, CoreApi, SessionUser, Version } from "@goauthentik/api";
autoDetectLanguage();
@ -175,10 +175,11 @@ export class AdminInterface extends Interface {
${this.user?.original
? html`<ak-sidebar-item
?highlight=${true}
?isAbsoluteLink=${true}
path=${`/-/impersonation/end/?back=${encodeURIComponent(
`${window.location.pathname}#${window.location.hash}`,
)}`}
@click=${() => {
new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
window.location.reload();
});
}}
>
<span slot="label"
>${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span

View File

@ -115,9 +115,8 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
`;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
@ -144,7 +143,6 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
>
</ak-search-select>
</ak-form-element-horizontal>
${this.result ? this.renderResult() : html``}
</form>`;
${this.result ? this.renderResult() : html``}`;
}
}

View File

@ -21,9 +21,12 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
});
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Common Name`} name="commonName" ?required=${true}>
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal
label=${t`Common Name`}
name="commonName"
?required=${true}
>
<input type="text" class="pf-c-form-control" required />
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName">
@ -38,7 +41,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
?required=${true}
>
<input class="pf-c-form-control" type="number" value="365" />
</ak-form-element-horizontal>
</form>`;
</ak-form-element-horizontal>`;
}
}

View File

@ -87,15 +87,13 @@ export class FlowImportForm extends Form<Flow> {
`;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Flow`} name="flow">
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${t`Flow`} name="flow">
<input type="file" value="" class="pf-c-form-control" />
<p class="pf-c-form__helper-text">
${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`}
</p>
</ak-form-element-horizontal>
${this.result ? this.renderResult() : html``}
</form>`;
${this.result ? this.renderResult() : html``}`;
}
}

View File

@ -46,41 +46,39 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
return data;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Groups to add`} name="groups">
<div class="pf-c-input-group">
<ak-user-group-select-table
.confirm=${(items: Group[]) => {
this.groupsToAdd = items;
this.requestUpdate();
return Promise.resolve();
}}
>
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
<i class="fas fa-plus" aria-hidden="true"></i>
</button>
</ak-user-group-select-table>
<div class="pf-c-form-control">
<ak-chip-group>
${this.groupsToAdd.map((group) => {
return html`<ak-chip
.removable=${true}
value=${ifDefined(group.pk)}
@remove=${() => {
const idx = this.groupsToAdd.indexOf(group);
this.groupsToAdd.splice(idx, 1);
this.requestUpdate();
}}
>
${group.name}
</ak-chip>`;
})}
</ak-chip-group>
</div>
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${t`Groups to add`} name="groups">
<div class="pf-c-input-group">
<ak-user-group-select-table
.confirm=${(items: Group[]) => {
this.groupsToAdd = items;
this.requestUpdate();
return Promise.resolve();
}}
>
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
<i class="fas fa-plus" aria-hidden="true"></i>
</button>
</ak-user-group-select-table>
<div class="pf-c-form-control">
<ak-chip-group>
${this.groupsToAdd.map((group) => {
return html`<ak-chip
.removable=${true}
value=${ifDefined(group.pk)}
@remove=${() => {
const idx = this.groupsToAdd.indexOf(group);
this.groupsToAdd.splice(idx, 1);
this.requestUpdate();
}}
>
${group.name}
</ak-chip>`;
})}
</ak-chip-group>
</div>
</ak-form-element-horizontal>
</form> `;
</div>
</ak-form-element-horizontal>`;
}
}

View File

@ -191,8 +191,12 @@ export class OutpostForm extends ModelForm<Outpost, string> {
const selected = Array.from(this.instance?.providers || []).some((sp) => {
return sp == provider.pk;
});
let appName = provider.assignedApplicationName;
if (provider.assignedBackchannelApplicationName) {
appName = provider.assignedBackchannelApplicationName;
}
return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>
${provider.assignedApplicationName} (${provider.name})
${appName} (${provider.name})
</option>`;
})}
</select>

View File

@ -116,9 +116,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
`;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
@ -155,7 +154,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
${t`Set custom attributes using YAML or JSON.`}
</p>
</ak-form-element-horizontal>
${this.result ? this.renderResult() : html``}
</form>`;
${this.result ? this.renderResult() : html``}`;
}
}

View File

@ -119,9 +119,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
`;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
@ -156,7 +155,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
</ak-codemirror>
<p class="pf-c-form__helper-text">${this.renderExampleButtons()}</p>
</ak-form-element-horizontal>
${this.result ? this.renderResult() : html``}
</form>`;
${this.result ? this.renderResult() : html``}`;
}
}

View File

@ -37,9 +37,8 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
});
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input type="text" class="pf-c-form-control" required />
</ak-form-element-horizontal>
<ak-form-element-horizontal
@ -77,7 +76,6 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
<ak-form-element-horizontal label=${t`Metadata`} name="metadata">
<input type="file" value="" class="pf-c-form-control" />
</ak-form-element-horizontal>
</form>`;
</ak-form-element-horizontal>`;
}
}

View File

@ -304,42 +304,6 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
${t`Keypair which is used to sign outgoing requests. Leave empty to disable signing.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Verification Certificate`}
name="verificationKp"
>
<ak-search-select
.fetchObjects=${async (
query?: string,
): Promise<CertificateKeyPair[]> => {
const args: CryptoCertificatekeypairsListRequest = {
ordering: "name",
includeDetails: false,
};
if (query !== undefined) {
args.search = query;
}
const certificates = await new CryptoApi(
DEFAULT_CONFIG,
).cryptoCertificatekeypairsList(args);
return certificates.results;
}}
.renderElement=${(item: CertificateKeyPair): string => {
return item.name;
}}
.value=${(item: CertificateKeyPair | undefined): string | undefined => {
return item?.pk;
}}
.selected=${(item: CertificateKeyPair): boolean => {
return item.pk === this.instance?.verificationKp;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${t`When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.`}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>

View File

@ -59,9 +59,8 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
return data;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
${this.group?.isSuperuser ? html`` : html``}
renderInlineForm(): TemplateResult {
return html`${this.group?.isSuperuser ? html`` : html``}
<ak-form-element-horizontal label=${t`Users to add`} name="users">
<div class="pf-c-input-group">
<ak-group-member-select-table
@ -93,8 +92,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
</ak-chip-group>
</div>
</div>
</ak-form-element-horizontal>
</form> `;
</ak-form-element-horizontal>`;
}
}
@ -193,12 +191,20 @@ export class RelatedUserList extends Table<User> {
</ak-forms-modal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
? html`
<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${item.pk}/`}"
<ak-action-button
class="pf-m-tertiary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: item.pk,
})
.then(() => {
window.location.href = "/";
});
}}
>
${t`Impersonate`}
</a>
</ak-action-button>
`
: html``}`,
];

View File

@ -35,9 +35,8 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
this.result = undefined;
}
renderRequestForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name">
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name">
<input type="text" value="" class="pf-c-form-control" required />
<p class="pf-c-form__helper-text">
${t`User's primary identifier. 150 characters or fewer.`}
@ -78,8 +77,7 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
</form>`;
</ak-form-element-horizontal>`;
}
renderResponseForm(): TemplateResult {
@ -113,6 +111,6 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
if (this.result) {
return this.renderResponseForm();
}
return this.renderRequestForm();
return super.renderForm();
}
}

View File

@ -196,12 +196,20 @@ export class UserListPage extends TablePage<User> {
</ak-forms-modal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
? html`
<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${item.pk}/`}"
<ak-action-button
class="pf-m-tertiary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: item.pk,
})
.then(() => {
window.location.href = "/";
});
}}
>
${t`Impersonate`}
</a>
</ak-action-button>
`
: html``}`,
];

View File

@ -26,11 +26,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
});
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Password`} ?required=${true} name="password">
<input type="password" value="" class="pf-c-form-control" required />
</ak-form-element-horizontal>
</form>`;
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal
label=${t`Password`}
?required=${true}
name="password"
>
<input type="password" value="" class="pf-c-form-control" required />
</ak-form-element-horizontal>`;
}
}

View File

@ -32,32 +32,34 @@ export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailRetrieveReque
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data);
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Email stage`} ?required=${true} name="emailStage">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
const args: StagesAllListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
return stages.results;
}}
.groupBy=${(items: Stage[]) => {
return groupBy(items, (stage) => stage.verboseNamePlural);
}}
.renderElement=${(stage: Stage): string => {
return stage.name;
}}
.value=${(stage: Stage | undefined): string | undefined => {
return stage?.pk;
}}
>
</ak-search-select>
</ak-form-element-horizontal>
</form>`;
renderInlineForm(): TemplateResult {
return html`<ak-form-element-horizontal
label=${t`Email stage`}
?required=${true}
name="emailStage"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
const args: StagesAllListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
return stages.results;
}}
.groupBy=${(items: Stage[]) => {
return groupBy(items, (stage) => stage.verboseNamePlural);
}}
.renderElement=${(stage: Stage): string => {
return stage.name;
}}
.value=${(stage: Stage | undefined): string | undefined => {
return stage?.pk;
}}
>
</ak-search-select>
</ak-form-element-horizontal>`;
}
}

View File

@ -201,12 +201,20 @@ export class UserViewPage extends AKElement {
)
? html`
<div class="pf-c-card__footer">
<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${this.user?.pk}/`}"
<ak-action-button
class="pf-m-tertiary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: this.user?.pk || 0,
})
.then(() => {
window.location.href = "/";
});
}}
>
${t`Impersonate`}
</a>
</ak-action-button>
</div>
`
: html``}

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 = "2023.5.2";
export const VERSION = "2023.5.6";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -46,6 +46,7 @@ export class Diagram extends AKElement {
flowchart: {
curve: "linear",
},
htmlLabels: false,
};
mermaid.initialize(this.config);
}

View File

@ -283,9 +283,23 @@ export abstract class Form<T> extends AKElement {
}
renderForm(): TemplateResult {
const inline = this.renderInlineForm();
if (inline) {
return html`<form class="pf-c-form pf-m-horizontal" @submit=${this.submit}>
${inline}
</form>`;
}
return html`<slot></slot>`;
}
/**
* Inline form render callback when inheriting this class, should be overwritten
* instead of `this.renderForm`
*/
renderInlineForm(): TemplateResult | undefined {
return undefined;
}
renderNonFieldErrors(): TemplateResult {
if (!this.nonFieldErrors) {
return html``;

View File

@ -202,7 +202,7 @@ export class IdentificationStage extends BaseStage<
}
renderInput(): TemplateResult {
let type: "text" | "email" = "text";
let type = "text";
if (!this.challenge?.userFields || this.challenge.userFields.length === 0) {
return html`<p>${t`Select one of the sources below to login.`}</p>`;
}

View File

@ -64,32 +64,32 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
/>`;
case PromptTypeEnum.TextArea:
return html`<textarea
type="text"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="off"
class="pf-c-form-control"
?required=${prompt.required}
>
${prompt.initialValue}</textarea
>`;
value="${prompt.initialValue}"
></textarea>`;
case PromptTypeEnum.TextReadOnly:
return html`<input
type="text"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?readonly=${true}
readonly
value="${prompt.initialValue}"
/>`;
case PromptTypeEnum.TextAreaReadOnly:
return html`<textarea
type="text"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
readonly
>
${prompt.initialValue}</textarea
>`;
value="${prompt.initialValue}"
></textarea>`;
case PromptTypeEnum.Username:
return html`<input
type="text"
@ -186,8 +186,8 @@ ${prompt.initialValue}</textarea
class="pf-c-check__input"
name="${prompt.fieldKey}"
id="${id}"
?checked="${prompt.initialValue === choice}"
?required="${prompt.required}"
checked="${prompt.initialValue === choice}"
required="${prompt.required}"
value="${choice}"
/>
<label class="pf-c-check__label" for=${id}>${choice}</label>
@ -195,7 +195,7 @@ ${prompt.initialValue}</textarea
})}`;
case PromptTypeEnum.AkLocale:
return html`<select class="pf-c-form-control" name="${prompt.fieldKey}">
<option value="" ?selected=${prompt.initialValue === ""}>
<option value="" ${prompt.initialValue === "" ? "selected" : ""}>
${t`Auto-detect (based on your browser)`}
</option>
${LOCALES.filter((locale) => {
@ -209,7 +209,7 @@ ${prompt.initialValue}</textarea
}).map((locale) => {
return html`<option
value=${locale.code}
?selected=${prompt.initialValue === locale.code}
${prompt.initialValue === locale.code ? "selected" : ""}
>
${locale.code.toUpperCase()} - ${locale.label}
</option>`;

View File

@ -7766,7 +7766,6 @@ msgid "Verification"
msgstr "Überprüfung"
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "Verification Certificate"
msgstr "Zertifikat zur Überprüfung"
@ -7992,7 +7991,6 @@ msgid "When selected, a password field is shown on the same page instead of a se
msgstr "Wenn diese Option ausgewählt ist, wird ein Passwortfeld auf derselben Seite statt auf einer separaten Seite angezeigt. Dadurch werden Angriffe auf die Aufzählung von Benutzernamen verhindert."
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "Wenn diese Option ausgewählt ist, werden die Signaturen eingehender Behauptungen anhand dieses Zertifikats validiert. Um nicht signierte Anfragen zuzulassen, belassen Sie die Standardeinstellung."

View File

@ -7930,7 +7930,6 @@ msgid "Verification"
msgstr "Verification"
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "Verification Certificate"
msgstr "Verification Certificate"
@ -8165,7 +8164,6 @@ msgid "When selected, a password field is shown on the same page instead of a se
msgstr "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks."
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."

View File

@ -7742,7 +7742,6 @@ msgid "Verification"
msgstr "Verificación"
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "Verification Certificate"
msgstr "Certificado de verificación"
@ -7968,7 +7967,6 @@ msgid "When selected, a password field is shown on the same page instead of a se
msgstr "Cuando se selecciona, se muestra un campo de contraseña en la misma página en lugar de en una página separada. Esto evita ataques de enumeración de nombres de usuario."
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "Cuando se selecciona, las firmas de la aserción entrante se validarán con este certificado. Para permitir solicitudes sin firmar, déjelo en el valor predeterminado."

View File

@ -7733,7 +7733,6 @@ msgid "Verification"
msgstr "Vérification"
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "Verification Certificate"
msgstr "Certificat de validation"
@ -7959,7 +7958,6 @@ msgid "When selected, a password field is shown on the same page instead of a se
msgstr "Si activée, un champ de mot de passe est affiché sur la même page au lieu d'une page séparée. Cela permet d'éviter les attaques par énumération de noms d'utilisateur."
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "Si activée, les signatures des assertions entrantes seront validées par rapport à ce certificat. Pour autoriser les requêtes non signées, laissez la valeur par défaut."

View File

@ -7752,7 +7752,6 @@ msgid "Verification"
msgstr "Weryfikacja"
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "Verification Certificate"
msgstr "Certyfikat weryfikacji"
@ -7980,7 +7979,6 @@ msgid "When selected, a password field is shown on the same page instead of a se
msgstr "Po wybraniu pole hasła jest wyświetlane na tej samej stronie zamiast na osobnej stronie. Zapobiega to atakom polegającym na wyliczaniu nazw użytkowników."
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "Po wybraniu, przychodzące podpisy asercji będą sprawdzane względem tego certyfikatu. Aby zezwolić na niepodpisane żądania, pozostaw domyślnie."

View File

@ -7888,7 +7888,6 @@ msgid "Verification"
msgstr ""
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "Verification Certificate"
msgstr ""
@ -8117,7 +8116,6 @@ msgid "When selected, a password field is shown on the same page instead of a se
msgstr ""
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr ""

View File

@ -7742,7 +7742,6 @@ msgid "Verification"
msgstr "Doğrulama"
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "Verification Certificate"
msgstr "Doğrulama Sertifikası"
@ -7968,7 +7967,6 @@ msgid "When selected, a password field is shown on the same page instead of a se
msgstr "Seçildiğinde, ayrı bir sayfa yerine aynı sayfada bir parola alanı gösterilir. Bu, kullanıcı adı numaralandırma saldırılarını engeller."
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "Seçildiğinde, gelen onaylama öğesinin İmzaları bu sertifikaya göre doğrulanır. İmzasız İsteklere izin vermek için varsayılan olarak bırakın."

View File

@ -757,7 +757,7 @@ msgstr "身份验证"
#: src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
msgid "Authentication code"
msgstr "身份验证代码"
msgstr ""
#: src/admin/providers/oauth2/OAuth2ProviderForm.ts
#: src/admin/providers/proxy/ProxyProviderForm.ts
@ -4473,7 +4473,7 @@ msgstr "打开设置"
#: src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
msgid "Open your two-factor authenticator app to view your authentication code."
msgstr "打开您的两步验证应用查看身份验证代码。"
msgstr ""
#: src/admin/providers/oauth2/OAuth2ProviderViewPage.ts
msgid "OpenID Configuration Issuer"
@ -4759,7 +4759,7 @@ msgstr "请输入您通过短信收到的验证码"
#: src/flow/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
msgid "Please enter your code"
msgstr "请输入您的代码"
msgstr ""
#: src/flow/providers/oauth2/DeviceCode.ts
msgid "Please enter your Code"
@ -7641,7 +7641,6 @@ msgid "Verification"
msgstr "验证"
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "Verification Certificate"
msgstr "验证证书"
@ -7725,7 +7724,7 @@ msgstr "警告:策略未分配。"
#: src/admin/providers/scim/SCIMProviderViewPage.ts
msgid "Warning: Provider is not assigned to an application as backchannel provider."
msgstr "警告:提供程序未作为反向通道分配给应用程序。"
msgstr ""
#: src/admin/providers/oauth2/OAuth2ProviderViewPage.ts
#: src/admin/providers/proxy/ProxyProviderViewPage.ts
@ -7869,7 +7868,6 @@ msgid "When selected, a password field is shown on the same page instead of a se
msgstr "选中后,密码字段将显示在同一页面,而不是单独的页面上。这样可以防止用户名枚举攻击。"
#: src/admin/providers/saml/SAMLProviderForm.ts
#: src/admin/sources/saml/SAMLSourceForm.ts
msgid "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default."
msgstr "选中后,传入断言的签名将根据此证书进行验证。要允许未签名的请求,请保留默认值。"

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