Compare commits
24 Commits
smusali/qr
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
3925f5a208 | |||
6add4a62b9 | |||
54d5aa20ba | |||
b99ac01228 | |||
15026748d1 | |||
2739376a2a | |||
152121175b | |||
1d57a258f3 | |||
f15cac39c8 | |||
ce77d82b24 | |||
c3fe57197d | |||
267938d435 | |||
6a7c2e0662 | |||
5336afb1b4 | |||
9bb44055a3 | |||
143663d293 | |||
bd54d034e1 | |||
be85eecac5 | |||
24385c9c68 | |||
e141a11475 | |||
b055adec2a | |||
772acb10d6 | |||
a7bf963409 | |||
317afc932a |
@ -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+)
|
||||
|
1
.github/workflows/ci-outpost.yml
vendored
1
.github/workflows/ci-outpost.yml
vendored
@ -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 }}
|
||||
|
7
.github/workflows/ghcr-retention.yml
vendored
7
.github/workflows/ghcr-retention.yml
vendored
@ -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
|
||||
|
1
.github/workflows/release-publish.yml
vendored
1
.github/workflows/release-publish.yml
vendored
@ -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
|
||||
|
9
.github/workflows/release-tag.yml
vendored
9
.github/workflows/release-tag.yml
vendored
@ -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 }}
|
||||
|
9
.github/workflows/translation-compile.yml
vendored
9
.github/workflows/translation-compile.yml
vendored
@ -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"
|
||||
|
11
.github/workflows/web-api-publish.yml
vendored
11
.github/workflows/web-api-publish.yml
vendored
@ -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
|
||||
|
@ -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"
|
||||
|
||||
|
||||
|
@ -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 = {}
|
||||
|
@ -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:
|
||||
|
@ -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]:
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
34
authentik/blueprints/tests/test_serializer_models.py
Normal file
34
authentik/blueprints/tests/test_serializer_models.py
Normal 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))
|
@ -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(
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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"""
|
||||
|
@ -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)
|
||||
|
@ -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/",
|
||||
|
@ -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")
|
@ -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}"
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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"]
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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"
|
@ -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,
|
||||
)
|
@ -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):
|
||||
|
@ -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]
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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}),
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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()])
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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": []
|
||||
|
@ -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
4
go.mod
@ -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
9
go.sum
@ -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=
|
||||
|
@ -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 {
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2023.5.2"
|
||||
const VERSION = "2023.5.6"
|
||||
|
44
internal/utils/web/http_forwarded.go
Normal file
44
internal/utils/web/http_forwarded.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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,
|
||||
|
@ -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)
|
@ -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 ""
|
||||
|
||||
|
@ -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 Webhook(Slack/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 ""
|
||||
|
@ -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 Webhook(Slack/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
284
poetry.lock
generated
@ -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]
|
||||
|
@ -113,7 +113,7 @@ filterwarnings = [
|
||||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2023.5.2"
|
||||
version = "2023.5.6"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
102
schema.yml
102
schema.yml
@ -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
1893
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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``}`;
|
||||
}
|
||||
}
|
||||
|
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
@ -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``}`;
|
||||
}
|
||||
}
|
||||
|
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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``}`;
|
||||
}
|
||||
}
|
||||
|
@ -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``}`;
|
||||
}
|
||||
}
|
||||
|
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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``}`,
|
||||
];
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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``}`,
|
||||
];
|
||||
|
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
@ -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``}
|
||||
|
@ -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 = ";";
|
||||
|
||||
|
@ -46,6 +46,7 @@ export class Diagram extends AKElement {
|
||||
flowchart: {
|
||||
curve: "linear",
|
||||
},
|
||||
htmlLabels: false,
|
||||
};
|
||||
mermaid.initialize(this.config);
|
||||
}
|
||||
|
@ -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``;
|
||||
|
@ -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>`;
|
||||
}
|
||||
|
@ -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>`;
|
||||
|
@ -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."
|
||||
|
||||
|
@ -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."
|
||||
|
||||
|
@ -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."
|
||||
|
||||
|
@ -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."
|
||||
|
||||
|
@ -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."
|
||||
|
||||
|
@ -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 ""
|
||||
|
||||
|
@ -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."
|
||||
|
||||
|
@ -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
Reference in New Issue
Block a user