Merge branch 'main' into web/flow/table-driven-executor
* main: (23 commits) website/docs: update info about footer links to match new UI (#12120) website/docs: prepare release notes (#12142) providers/oauth2: fix migration (#12138) providers/oauth2: fix migration dependencies (#12123) web: bump API Client version (#12129) providers/oauth2: fix redirect uri input (#12122) providers/proxy: fix redirect_uri (#12121) website/docs: prepare release notes (#12119) web: bump API Client version (#12118) security: fix CVE 2024 52289 (#12113) security: fix CVE 2024 52307 (#12115) security: fix CVE 2024 52287 (#12114) website/docs: add CSP to hardening (#11970) core: bump uvicorn from 0.32.0 to 0.32.1 (#12103) core: bump google-api-python-client from 2.153.0 to 2.154.0 (#12104) core: bump pydantic from 2.9.2 to 2.10.0 (#12105) translate: Updates for file locale/en/LC_MESSAGES/django.po in it (#12110) internal: add CSP header to files in `/media` (#12092) core, web: update translations (#12101) web: fix bug that prevented error reporting in current wizard. (#12033) ...
This commit is contained in:
@ -4,7 +4,7 @@ from collections.abc import Callable
|
|||||||
|
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.utils.translation import activate
|
from django.utils.translation import override
|
||||||
|
|
||||||
from authentik.brands.utils import get_brand_for_request
|
from authentik.brands.utils import get_brand_for_request
|
||||||
|
|
||||||
@ -18,10 +18,12 @@ class BrandMiddleware:
|
|||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
locale_to_set = None
|
||||||
if not hasattr(request, "brand"):
|
if not hasattr(request, "brand"):
|
||||||
brand = get_brand_for_request(request)
|
brand = get_brand_for_request(request)
|
||||||
request.brand = brand
|
request.brand = brand
|
||||||
locale = brand.default_locale
|
locale = brand.default_locale
|
||||||
if locale != "":
|
if locale != "":
|
||||||
activate(locale)
|
locale_to_set = locale
|
||||||
return self.get_response(request)
|
with override(locale_to_set):
|
||||||
|
return self.get_response(request)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from contextvars import ContextVar
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import activate
|
from django.utils.translation import override
|
||||||
from sentry_sdk.api import set_tag
|
from sentry_sdk.api import set_tag
|
||||||
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
||||||
|
|
||||||
@ -31,17 +31,19 @@ class ImpersonateMiddleware:
|
|||||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
# No permission checks are done here, they need to be checked before
|
# No permission checks are done here, they need to be checked before
|
||||||
# SESSION_KEY_IMPERSONATE_USER is set.
|
# SESSION_KEY_IMPERSONATE_USER is set.
|
||||||
|
locale_to_set = None
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
locale = request.user.locale(request)
|
locale = request.user.locale(request)
|
||||||
if locale != "":
|
if locale != "":
|
||||||
activate(locale)
|
locale_to_set = locale
|
||||||
|
|
||||||
if SESSION_KEY_IMPERSONATE_USER in request.session:
|
if SESSION_KEY_IMPERSONATE_USER in request.session:
|
||||||
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
|
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||||
# Ensure that the user is active, otherwise nothing will work
|
# Ensure that the user is active, otherwise nothing will work
|
||||||
request.user.is_active = True
|
request.user.is_active = True
|
||||||
|
|
||||||
return self.get_response(request)
|
with override(locale_to_set):
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|
||||||
class RequestIDMiddleware:
|
class RequestIDMiddleware:
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
|||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
from authentik.providers.saml.models import SAMLProvider
|
from authentik.providers.saml.models import SAMLProvider
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
redirect_uris="http://some-other-domain",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://some-other-domain")],
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
)
|
)
|
||||||
self.allowed: Application = Application.objects.create(
|
self.allowed: Application = Application.objects.create(
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"name": uid,
|
"name": uid,
|
||||||
"authorization_flow": str(create_test_flow().pk),
|
"authorization_flow": str(create_test_flow().pk),
|
||||||
"invalidation_flow": str(create_test_flow().pk),
|
"invalidation_flow": str(create_test_flow().pk),
|
||||||
|
"redirect_uris": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -89,6 +90,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"name": uid,
|
"name": uid,
|
||||||
"authorization_flow": str(authorization_flow.pk),
|
"authorization_flow": str(authorization_flow.pk),
|
||||||
"invalidation_flow": str(authorization_flow.pk),
|
"invalidation_flow": str(authorization_flow.pk),
|
||||||
|
"redirect_uris": [],
|
||||||
},
|
},
|
||||||
"policy_bindings": [{"group": group.pk, "order": 0}],
|
"policy_bindings": [{"group": group.pk, "order": 0}],
|
||||||
},
|
},
|
||||||
@ -120,6 +122,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"name": uid,
|
"name": uid,
|
||||||
"authorization_flow": "",
|
"authorization_flow": "",
|
||||||
"invalidation_flow": "",
|
"invalidation_flow": "",
|
||||||
|
"redirect_uris": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -18,7 +18,7 @@ from authentik.crypto.models import CertificateKeyPair
|
|||||||
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
|
||||||
|
|
||||||
|
|
||||||
class TestCrypto(APITestCase):
|
class TestCrypto(APITestCase):
|
||||||
@ -274,7 +274,7 @@ class TestCrypto(APITestCase):
|
|||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_key(),
|
client_secret=generate_key(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=keypair,
|
signing_key=keypair,
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@ -306,7 +306,7 @@ class TestCrypto(APITestCase):
|
|||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_key(),
|
client_secret=generate_key(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=keypair,
|
signing_key=keypair,
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
"""OAuth2Provider API Views"""
|
"""OAuth2Provider API Views"""
|
||||||
|
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
from re import compile
|
||||||
|
from re import error as RegexError
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField, ChoiceField
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -20,13 +23,39 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
|
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.providers.oauth2.id_token import IDToken
|
from authentik.providers.oauth2.id_token import IDToken
|
||||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectURISerializer(PassiveSerializer):
|
||||||
|
"""A single allowed redirect URI entry"""
|
||||||
|
|
||||||
|
matching_mode = ChoiceField(choices=RedirectURIMatchingMode.choices)
|
||||||
|
url = CharField()
|
||||||
|
|
||||||
|
|
||||||
class OAuth2ProviderSerializer(ProviderSerializer):
|
class OAuth2ProviderSerializer(ProviderSerializer):
|
||||||
"""OAuth2Provider Serializer"""
|
"""OAuth2Provider Serializer"""
|
||||||
|
|
||||||
|
redirect_uris = RedirectURISerializer(many=True, source="_redirect_uris")
|
||||||
|
|
||||||
|
def validate_redirect_uris(self, data: list) -> list:
|
||||||
|
for entry in data:
|
||||||
|
if entry.get("matching_mode") == RedirectURIMatchingMode.REGEX:
|
||||||
|
url = entry.get("url")
|
||||||
|
try:
|
||||||
|
compile(url)
|
||||||
|
except RegexError:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Invalid Regex Pattern: {url}".format(url=url))
|
||||||
|
) from None
|
||||||
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OAuth2Provider
|
model = OAuth2Provider
|
||||||
fields = ProviderSerializer.Meta.fields + [
|
fields = ProviderSerializer.Meta.fields + [
|
||||||
@ -79,7 +108,6 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"refresh_token_validity",
|
"refresh_token_validity",
|
||||||
"include_claims_in_id_token",
|
"include_claims_in_id_token",
|
||||||
"signing_key",
|
"signing_key",
|
||||||
"redirect_uris",
|
|
||||||
"sub_mode",
|
"sub_mode",
|
||||||
"property_mappings",
|
"property_mappings",
|
||||||
"issuer_mode",
|
"issuer_mode",
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
|||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.providers.oauth2.models import GrantTypes
|
from authentik.providers.oauth2.models import GrantTypes, RedirectURI
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Error(SentryIgnoredException):
|
class OAuth2Error(SentryIgnoredException):
|
||||||
@ -46,9 +46,9 @@ class RedirectUriError(OAuth2Error):
|
|||||||
)
|
)
|
||||||
|
|
||||||
provided_uri: str
|
provided_uri: str
|
||||||
allowed_uris: list[str]
|
allowed_uris: list[RedirectURI]
|
||||||
|
|
||||||
def __init__(self, provided_uri: str, allowed_uris: list[str]) -> None:
|
def __init__(self, provided_uri: str, allowed_uris: list[RedirectURI]) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.provided_uri = provided_uri
|
self.provided_uri = provided_uri
|
||||||
self.allowed_uris = allowed_uris
|
self.allowed_uris = allowed_uris
|
||||||
|
|||||||
@ -37,7 +37,7 @@ def migrate_session(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0040_provider_invalidation_flow"),
|
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
|
||||||
("authentik_providers_oauth2", "0021_oauth2provider_encryption_key_and_more"),
|
("authentik_providers_oauth2", "0021_oauth2provider_encryption_key_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from django.db import migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_core", "0040_provider_invalidation_flow"),
|
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
|
||||||
("authentik_providers_oauth2", "0022_remove_accesstoken_session_id_and_more"),
|
("authentik_providers_oauth2", "0022_remove_accesstoken_session_id_and_more"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-11-04 12:56
|
||||||
|
from dataclasses import asdict
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_redirect_uris(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
from authentik.providers.oauth2.models import RedirectURI, RedirectURIMatchingMode
|
||||||
|
|
||||||
|
OAuth2Provider = apps.get_model("authentik_providers_oauth2", "oauth2provider")
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for provider in OAuth2Provider.objects.using(db_alias).all():
|
||||||
|
uris = []
|
||||||
|
for old in provider.old_redirect_uris.split("\n"):
|
||||||
|
mode = RedirectURIMatchingMode.STRICT
|
||||||
|
if old == "*" or old == ".*":
|
||||||
|
mode = RedirectURIMatchingMode.REGEX
|
||||||
|
uris.append(asdict(RedirectURI(mode, url=old)))
|
||||||
|
provider._redirect_uris = uris
|
||||||
|
provider.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_providers_oauth2", "0023_alter_accesstoken_refreshtoken_use_hash_index"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="oauth2provider",
|
||||||
|
old_name="redirect_uris",
|
||||||
|
new_name="old_redirect_uris",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="oauth2provider",
|
||||||
|
name="_redirect_uris",
|
||||||
|
field=models.JSONField(default=dict, verbose_name="Redirect URIs"),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_redirect_uris, lambda *args: ...),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="oauth2provider",
|
||||||
|
name="old_redirect_uris",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import json
|
import json
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict, dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -12,6 +12,7 @@ from urllib.parse import urlparse, urlunparse
|
|||||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
|
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||||
|
from dacite import Config
|
||||||
from dacite.core import from_dict
|
from dacite.core import from_dict
|
||||||
from django.contrib.postgres.indexes import HashIndex
|
from django.contrib.postgres.indexes import HashIndex
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -77,11 +78,25 @@ class IssuerMode(models.TextChoices):
|
|||||||
"""Configure how the `iss` field is created."""
|
"""Configure how the `iss` field is created."""
|
||||||
|
|
||||||
GLOBAL = "global", _("Same identifier is used for all providers")
|
GLOBAL = "global", _("Same identifier is used for all providers")
|
||||||
PER_PROVIDER = "per_provider", _(
|
PER_PROVIDER = (
|
||||||
"Each provider has a different issuer, based on the application slug."
|
"per_provider",
|
||||||
|
_("Each provider has a different issuer, based on the application slug."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectURIMatchingMode(models.TextChoices):
|
||||||
|
STRICT = "strict", _("Strict URL comparison")
|
||||||
|
REGEX = "regex", _("Regular Expression URL matching")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RedirectURI:
|
||||||
|
"""A single redirect URI entry"""
|
||||||
|
|
||||||
|
matching_mode: RedirectURIMatchingMode
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
class ResponseTypes(models.TextChoices):
|
class ResponseTypes(models.TextChoices):
|
||||||
"""Response Type required by the client."""
|
"""Response Type required by the client."""
|
||||||
|
|
||||||
@ -156,11 +171,9 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
|||||||
verbose_name=_("Client Secret"),
|
verbose_name=_("Client Secret"),
|
||||||
default=generate_client_secret,
|
default=generate_client_secret,
|
||||||
)
|
)
|
||||||
redirect_uris = models.TextField(
|
_redirect_uris = models.JSONField(
|
||||||
default="",
|
default=dict,
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Redirect URIs"),
|
verbose_name=_("Redirect URIs"),
|
||||||
help_text=_("Enter each URI on a new line."),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
include_claims_in_id_token = models.BooleanField(
|
include_claims_in_id_token = models.BooleanField(
|
||||||
@ -271,12 +284,33 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
|||||||
except Provider.application.RelatedObjectDoesNotExist:
|
except Provider.application.RelatedObjectDoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def redirect_uris(self) -> list[RedirectURI]:
|
||||||
|
uris = []
|
||||||
|
for entry in self._redirect_uris:
|
||||||
|
uris.append(
|
||||||
|
from_dict(
|
||||||
|
RedirectURI,
|
||||||
|
entry,
|
||||||
|
config=Config(type_hooks={RedirectURIMatchingMode: RedirectURIMatchingMode}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return uris
|
||||||
|
|
||||||
|
@redirect_uris.setter
|
||||||
|
def redirect_uris(self, value: list[RedirectURI]):
|
||||||
|
cleansed = []
|
||||||
|
for entry in value:
|
||||||
|
cleansed.append(asdict(entry))
|
||||||
|
self._redirect_uris = cleansed
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def launch_url(self) -> str | None:
|
def launch_url(self) -> str | None:
|
||||||
"""Guess launch_url based on first redirect_uri"""
|
"""Guess launch_url based on first redirect_uri"""
|
||||||
if self.redirect_uris == "":
|
redirects = self.redirect_uris
|
||||||
|
if len(redirects) < 1:
|
||||||
return None
|
return None
|
||||||
main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
|
main_url = redirects[0].url
|
||||||
try:
|
try:
|
||||||
launch_url = urlparse(main_url)._replace(path="")
|
launch_url = urlparse(main_url)._replace(path="")
|
||||||
return urlunparse(launch_url)
|
return urlunparse(launch_url)
|
||||||
|
|||||||
@ -10,7 +10,13 @@ from rest_framework.test import APITestCase
|
|||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.providers.oauth2.models import (
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestAPI(APITestCase):
|
class TestAPI(APITestCase):
|
||||||
@ -21,7 +27,7 @@ class TestAPI(APITestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
|
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
|
||||||
@ -50,9 +56,29 @@ class TestAPI(APITestCase):
|
|||||||
@skipUnless(version_info >= (3, 11, 4), "This behaviour is only Python 3.11.4 and up")
|
@skipUnless(version_info >= (3, 11, 4), "This behaviour is only Python 3.11.4 and up")
|
||||||
def test_launch_url(self):
|
def test_launch_url(self):
|
||||||
"""Test launch_url"""
|
"""Test launch_url"""
|
||||||
self.provider.redirect_uris = (
|
self.provider.redirect_uris = [
|
||||||
"https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/\n"
|
RedirectURI(
|
||||||
)
|
RedirectURIMatchingMode.REGEX,
|
||||||
|
"https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/",
|
||||||
|
),
|
||||||
|
]
|
||||||
self.provider.save()
|
self.provider.save()
|
||||||
self.provider.refresh_from_db()
|
self.provider.refresh_from_db()
|
||||||
self.assertIsNone(self.provider.launch_url)
|
self.assertIsNone(self.provider.launch_url)
|
||||||
|
|
||||||
|
def test_validate_redirect_uris(self):
|
||||||
|
"""Test redirect_uris API"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:oauth2provider-list"),
|
||||||
|
data={
|
||||||
|
"name": generate_id(),
|
||||||
|
"authorization_flow": create_test_flow().pk,
|
||||||
|
"invalidation_flow": create_test_flow().pk,
|
||||||
|
"redirect_uris": [
|
||||||
|
{"matching_mode": "strict", "url": "http://goauthentik.io"},
|
||||||
|
{"matching_mode": "regex", "url": "**"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertJSONEqual(response.content, {"redirect_uris": ["Invalid Regex Pattern: **"]})
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|||||||
@ -19,6 +19,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
GrantTypes,
|
GrantTypes,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
@ -39,7 +41,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid/Foo",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(AuthorizeError):
|
with self.assertRaises(AuthorizeError):
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
@ -64,7 +66,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid/Foo",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(AuthorizeError):
|
with self.assertRaises(AuthorizeError):
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
@ -84,7 +86,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||||
@ -106,7 +108,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="data:local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:local.invalid")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
@ -125,7 +127,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||||
@ -140,7 +142,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
)
|
)
|
||||||
OAuthAuthorizationParams.from_request(request)
|
OAuthAuthorizationParams.from_request(request)
|
||||||
provider.refresh_from_db()
|
provider.refresh_from_db()
|
||||||
self.assertEqual(provider.redirect_uris, "+")
|
self.assertEqual(provider.redirect_uris, [RedirectURI(RedirectURIMatchingMode.STRICT, "+")])
|
||||||
|
|
||||||
def test_invalid_redirect_uri_regex(self):
|
def test_invalid_redirect_uri_regex(self):
|
||||||
"""test missing/invalid redirect URI"""
|
"""test missing/invalid redirect URI"""
|
||||||
@ -148,7 +150,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid?",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid?")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||||
@ -170,7 +172,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="+",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "+")],
|
||||||
)
|
)
|
||||||
with self.assertRaises(RedirectUriError):
|
with self.assertRaises(RedirectUriError):
|
||||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||||
@ -213,7 +215,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid/Foo",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -301,7 +303,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -343,7 +345,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -420,7 +422,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
encryption_key=self.keypair,
|
encryption_key=self.keypair,
|
||||||
)
|
)
|
||||||
@ -486,7 +488,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -541,7 +543,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
client_id=generate_id(),
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -599,7 +601,7 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
client_id=generate_id(),
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||||
|
|||||||
@ -11,7 +11,14 @@ from authentik.core.models import Application
|
|||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
||||||
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
IDToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
RefreshToken,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -23,7 +30,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.app = Application.objects.create(
|
self.app = Application.objects.create(
|
||||||
@ -118,7 +125,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||||||
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from authentik.core.tests.utils import create_test_cert, create_test_flow
|
|||||||
from authentik.crypto.builder import PrivateKeyAlg
|
from authentik.crypto.builder import PrivateKeyAlg
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
TEST_CORDS_CERT = """
|
TEST_CORDS_CERT = """
|
||||||
@ -49,7 +49,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
@ -68,7 +68,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@ -82,7 +82,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
@ -99,7 +99,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
||||||
encryption_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
encryption_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
||||||
)
|
)
|
||||||
@ -122,7 +122,7 @@ class TestJWKS(OAuthTestCase):
|
|||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=cert,
|
signing_key=cert,
|
||||||
)
|
)
|
||||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||||
|
|||||||
@ -10,7 +10,14 @@ from django.utils import timezone
|
|||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
IDToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
RefreshToken,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +29,7 @@ class TesOAuth2Revoke(OAuthTestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.app = Application.objects.create(
|
self.app = Application.objects.create(
|
||||||
|
|||||||
@ -22,6 +22,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
AccessToken,
|
AccessToken,
|
||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
@ -42,7 +44,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://TestServer",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://TestServer")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
@ -69,7 +71,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
@ -90,7 +92,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||||
@ -118,7 +120,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
# Needs to be assigned to an application for iss to be set
|
# Needs to be assigned to an application for iss to be set
|
||||||
@ -157,7 +159,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
encryption_key=self.keypair,
|
encryption_key=self.keypair,
|
||||||
)
|
)
|
||||||
@ -188,7 +190,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -250,7 +252,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -308,7 +310,7 @@ class TestToken(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
|
|||||||
@ -19,7 +19,12 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID_PROFILE,
|
SCOPE_OPENID_PROFILE,
|
||||||
TOKEN_TYPE,
|
TOKEN_TYPE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
from authentik.providers.oauth2.views.jwks import JWKSView
|
from authentik.providers.oauth2.views.jwks import JWKSView
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
@ -54,7 +59,7 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=self.cert,
|
signing_key=self.cert,
|
||||||
)
|
)
|
||||||
self.provider.jwks_sources.add(self.source)
|
self.provider.jwks_sources.add(self.source)
|
||||||
|
|||||||
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
|||||||
TOKEN_TYPE,
|
TOKEN_TYPE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.errors import TokenError
|
from authentik.providers.oauth2.errors import TokenError
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +39,7 @@ class TestTokenClientCredentialsStandard(OAuthTestCase):
|
|||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
@ -107,6 +113,48 @@ class TestTokenClientCredentialsStandard(OAuthTestCase):
|
|||||||
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_incorrect_scopes(self):
|
||||||
|
"""test scope that isn't configured"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_providers_oauth2:token"),
|
||||||
|
{
|
||||||
|
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
|
||||||
|
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE} extra_scope",
|
||||||
|
"client_id": self.provider.client_id,
|
||||||
|
"client_secret": self.provider.client_secret,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
body = loads(response.content.decode())
|
||||||
|
self.assertEqual(body["token_type"], TOKEN_TYPE)
|
||||||
|
token = AccessToken.objects.filter(
|
||||||
|
provider=self.provider, token=body["access_token"]
|
||||||
|
).first()
|
||||||
|
self.assertSetEqual(
|
||||||
|
set(token.scope), {SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE}
|
||||||
|
)
|
||||||
|
_, alg = self.provider.jwt_key
|
||||||
|
jwt = decode(
|
||||||
|
body["access_token"],
|
||||||
|
key=self.provider.signing_key.public_key,
|
||||||
|
algorithms=[alg],
|
||||||
|
audience=self.provider.client_id,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
jwt["given_name"], "Autogenerated user from application test (client credentials)"
|
||||||
|
)
|
||||||
|
self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
|
||||||
|
jwt = decode(
|
||||||
|
body["id_token"],
|
||||||
|
key=self.provider.signing_key.public_key,
|
||||||
|
algorithms=[alg],
|
||||||
|
audience=self.provider.client_id,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
jwt["given_name"], "Autogenerated user from application test (client credentials)"
|
||||||
|
)
|
||||||
|
self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
|
||||||
|
|
||||||
def test_successful(self):
|
def test_successful(self):
|
||||||
"""test successful"""
|
"""test successful"""
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
|
|||||||
@ -20,7 +20,12 @@ from authentik.providers.oauth2.constants import (
|
|||||||
TOKEN_TYPE,
|
TOKEN_TYPE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.errors import TokenError
|
from authentik.providers.oauth2.errors import TokenError
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -34,7 +39,7 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
|
|||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
|
|||||||
@ -19,7 +19,12 @@ from authentik.providers.oauth2.constants import (
|
|||||||
TOKEN_TYPE,
|
TOKEN_TYPE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.errors import TokenError
|
from authentik.providers.oauth2.errors import TokenError
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +38,7 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
|
|||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
|
|||||||
@ -9,8 +9,19 @@ from authentik.blueprints.tests import apply_blueprint
|
|||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_code_fixed_length, generate_id
|
from authentik.lib.generators import generate_code_fixed_length, generate_id
|
||||||
from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE
|
from authentik.providers.oauth2.constants import (
|
||||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping
|
GRANT_TYPE_DEVICE_CODE,
|
||||||
|
SCOPE_OPENID,
|
||||||
|
SCOPE_OPENID_EMAIL,
|
||||||
|
)
|
||||||
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
DeviceToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +35,7 @@ class TestTokenDeviceCode(OAuthTestCase):
|
|||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
@ -80,3 +91,28 @@ class TestTokenDeviceCode(OAuthTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(res.status_code, 200)
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
def test_code_mismatched_scope(self):
|
||||||
|
"""Test code with user (mismatched scopes)"""
|
||||||
|
device_token = DeviceToken.objects.create(
|
||||||
|
provider=self.provider,
|
||||||
|
user_code=generate_code_fixed_length(),
|
||||||
|
device_code=generate_id(),
|
||||||
|
user=self.user,
|
||||||
|
scope=[SCOPE_OPENID, SCOPE_OPENID_EMAIL],
|
||||||
|
)
|
||||||
|
res = self.client.post(
|
||||||
|
reverse("authentik_providers_oauth2:token"),
|
||||||
|
data={
|
||||||
|
"client_id": self.provider.client_id,
|
||||||
|
"grant_type": GRANT_TYPE_DEVICE_CODE,
|
||||||
|
"device_code": device_token.device_code,
|
||||||
|
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} invalid",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
body = loads(res.content)
|
||||||
|
token = AccessToken.objects.filter(
|
||||||
|
provider=self.provider, token=body["access_token"]
|
||||||
|
).first()
|
||||||
|
self.assertSetEqual(set(token.scope), {SCOPE_OPENID, SCOPE_OPENID_EMAIL})
|
||||||
|
|||||||
@ -10,7 +10,12 @@ from authentik.core.models import Application
|
|||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE
|
from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE
|
||||||
from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider
|
from authentik.providers.oauth2.models import (
|
||||||
|
AuthorizationCode,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -30,7 +35,7 @@ class TestTokenPKCE(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -93,7 +98,7 @@ class TestTokenPKCE(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -154,7 +159,7 @@ class TestTokenPKCE(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
@ -210,7 +215,7 @@ class TestTokenPKCE(OAuthTestCase):
|
|||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="foo://localhost",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||||
access_code_validity="seconds=100",
|
access_code_validity="seconds=100",
|
||||||
)
|
)
|
||||||
Application.objects.create(name="app", slug="app", provider=provider)
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
|
|||||||
@ -11,7 +11,14 @@ from authentik.core.models import Application
|
|||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
AccessToken,
|
||||||
|
IDToken,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +32,7 @@ class TestUserinfo(OAuthTestCase):
|
|||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
)
|
)
|
||||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||||
|
|||||||
@ -56,6 +56,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
GrantTypes,
|
GrantTypes,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
ResponseMode,
|
ResponseMode,
|
||||||
ResponseTypes,
|
ResponseTypes,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
@ -187,40 +189,39 @@ class OAuthAuthorizationParams:
|
|||||||
|
|
||||||
def check_redirect_uri(self):
|
def check_redirect_uri(self):
|
||||||
"""Redirect URI validation."""
|
"""Redirect URI validation."""
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
allowed_redirect_urls = self.provider.redirect_uris
|
||||||
if not self.redirect_uri:
|
if not self.redirect_uri:
|
||||||
LOGGER.warning("Missing redirect uri.")
|
LOGGER.warning("Missing redirect uri.")
|
||||||
raise RedirectUriError("", allowed_redirect_urls)
|
raise RedirectUriError("", allowed_redirect_urls)
|
||||||
|
|
||||||
if self.provider.redirect_uris == "":
|
if len(allowed_redirect_urls) < 1:
|
||||||
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
|
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
|
||||||
self.provider.redirect_uris = self.redirect_uri
|
self.provider.redirect_uris = [
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, self.redirect_uri)
|
||||||
|
]
|
||||||
self.provider.save()
|
self.provider.save()
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
allowed_redirect_urls = self.provider.redirect_uris
|
||||||
|
|
||||||
if self.provider.redirect_uris == "*":
|
match_found = False
|
||||||
LOGGER.info("Converting redirect_uris to regex", redirect=self.redirect_uri)
|
for allowed in allowed_redirect_urls:
|
||||||
self.provider.redirect_uris = ".*"
|
if allowed.matching_mode == RedirectURIMatchingMode.STRICT:
|
||||||
self.provider.save()
|
if self.redirect_uri == allowed.url:
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
match_found = True
|
||||||
|
break
|
||||||
try:
|
if allowed.matching_mode == RedirectURIMatchingMode.REGEX:
|
||||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
try:
|
||||||
LOGGER.warning(
|
if fullmatch(allowed.url, self.redirect_uri):
|
||||||
"Invalid redirect uri (regex comparison)",
|
match_found = True
|
||||||
redirect_uri_given=self.redirect_uri,
|
break
|
||||||
redirect_uri_expected=allowed_redirect_urls,
|
except RegexError as exc:
|
||||||
)
|
LOGGER.warning(
|
||||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
"Failed to parse regular expression",
|
||||||
except RegexError as exc:
|
exc=exc,
|
||||||
LOGGER.info("Failed to parse regular expression, checking directly", exc=exc)
|
url=allowed.url,
|
||||||
if not any(x == self.redirect_uri for x in allowed_redirect_urls):
|
provider=self.provider,
|
||||||
LOGGER.warning(
|
)
|
||||||
"Invalid redirect uri (strict comparison)",
|
if not match_found:
|
||||||
redirect_uri_given=self.redirect_uri,
|
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||||
redirect_uri_expected=allowed_redirect_urls,
|
|
||||||
)
|
|
||||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) from None
|
|
||||||
# Check against forbidden schemes
|
# Check against forbidden schemes
|
||||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||||
|
|||||||
@ -162,5 +162,5 @@ class ProviderInfoView(View):
|
|||||||
OAuth2Provider, pk=application.provider_id
|
OAuth2Provider, pk=application.provider_id
|
||||||
)
|
)
|
||||||
response = super().dispatch(request, *args, **kwargs)
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
cors_allow(request, response, *self.provider.redirect_uris.split("\n"))
|
cors_allow(request, response, *[x.url for x in self.provider.redirect_uris])
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -58,7 +58,9 @@ from authentik.providers.oauth2.models import (
|
|||||||
ClientTypes,
|
ClientTypes,
|
||||||
DeviceToken,
|
DeviceToken,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
|
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
|
||||||
from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES
|
from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES
|
||||||
@ -77,7 +79,7 @@ class TokenParams:
|
|||||||
redirect_uri: str
|
redirect_uri: str
|
||||||
grant_type: str
|
grant_type: str
|
||||||
state: str
|
state: str
|
||||||
scope: list[str]
|
scope: set[str]
|
||||||
|
|
||||||
provider: OAuth2Provider
|
provider: OAuth2Provider
|
||||||
|
|
||||||
@ -112,11 +114,26 @@ class TokenParams:
|
|||||||
redirect_uri=request.POST.get("redirect_uri", ""),
|
redirect_uri=request.POST.get("redirect_uri", ""),
|
||||||
grant_type=request.POST.get("grant_type", ""),
|
grant_type=request.POST.get("grant_type", ""),
|
||||||
state=request.POST.get("state", ""),
|
state=request.POST.get("state", ""),
|
||||||
scope=request.POST.get("scope", "").split(),
|
scope=set(request.POST.get("scope", "").split()),
|
||||||
# PKCE parameter.
|
# PKCE parameter.
|
||||||
code_verifier=request.POST.get("code_verifier"),
|
code_verifier=request.POST.get("code_verifier"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __check_scopes(self):
|
||||||
|
allowed_scope_names = set(
|
||||||
|
ScopeMapping.objects.filter(provider__in=[self.provider]).values_list(
|
||||||
|
"scope_name", flat=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
scopes_to_check = self.scope
|
||||||
|
if not scopes_to_check.issubset(allowed_scope_names):
|
||||||
|
LOGGER.info(
|
||||||
|
"Application requested scopes not configured, setting to overlap",
|
||||||
|
scope_allowed=allowed_scope_names,
|
||||||
|
scope_given=self.scope,
|
||||||
|
)
|
||||||
|
self.scope = self.scope.intersection(allowed_scope_names)
|
||||||
|
|
||||||
def __check_policy_access(self, app: Application, request: HttpRequest, **kwargs):
|
def __check_policy_access(self, app: Application, request: HttpRequest, **kwargs):
|
||||||
with start_span(
|
with start_span(
|
||||||
op="authentik.providers.oauth2.token.policy",
|
op="authentik.providers.oauth2.token.policy",
|
||||||
@ -149,7 +166,7 @@ class TokenParams:
|
|||||||
client_id=self.provider.client_id,
|
client_id=self.provider.client_id,
|
||||||
)
|
)
|
||||||
raise TokenError("invalid_client")
|
raise TokenError("invalid_client")
|
||||||
|
self.__check_scopes()
|
||||||
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||||
with start_span(
|
with start_span(
|
||||||
op="authentik.providers.oauth2.post.parse.code",
|
op="authentik.providers.oauth2.post.parse.code",
|
||||||
@ -179,42 +196,7 @@ class TokenParams:
|
|||||||
LOGGER.warning("Missing authorization code")
|
LOGGER.warning("Missing authorization code")
|
||||||
raise TokenError("invalid_grant")
|
raise TokenError("invalid_grant")
|
||||||
|
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
self.__check_redirect_uri(request)
|
||||||
# At this point, no provider should have a blank redirect_uri, in case they do
|
|
||||||
# this will check an empty array and raise an error
|
|
||||||
try:
|
|
||||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
|
||||||
LOGGER.warning(
|
|
||||||
"Invalid redirect uri (regex comparison)",
|
|
||||||
redirect_uri=self.redirect_uri,
|
|
||||||
expected=allowed_redirect_urls,
|
|
||||||
)
|
|
||||||
Event.new(
|
|
||||||
EventAction.CONFIGURATION_ERROR,
|
|
||||||
message="Invalid redirect URI used by provider",
|
|
||||||
provider=self.provider,
|
|
||||||
redirect_uri=self.redirect_uri,
|
|
||||||
expected=allowed_redirect_urls,
|
|
||||||
).from_http(request)
|
|
||||||
raise TokenError("invalid_client")
|
|
||||||
except RegexError as exc:
|
|
||||||
LOGGER.info("Failed to parse regular expression, checking directly", exc=exc)
|
|
||||||
if not any(x == self.redirect_uri for x in allowed_redirect_urls):
|
|
||||||
LOGGER.warning(
|
|
||||||
"Invalid redirect uri (strict comparison)",
|
|
||||||
redirect_uri=self.redirect_uri,
|
|
||||||
expected=allowed_redirect_urls,
|
|
||||||
)
|
|
||||||
Event.new(
|
|
||||||
EventAction.CONFIGURATION_ERROR,
|
|
||||||
message="Invalid redirect_uri configured",
|
|
||||||
provider=self.provider,
|
|
||||||
).from_http(request)
|
|
||||||
raise TokenError("invalid_client") from None
|
|
||||||
|
|
||||||
# Check against forbidden schemes
|
|
||||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
|
||||||
raise TokenError("invalid_request")
|
|
||||||
|
|
||||||
self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
|
self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
|
||||||
if not self.authorization_code:
|
if not self.authorization_code:
|
||||||
@ -254,6 +236,48 @@ class TokenParams:
|
|||||||
if not self.authorization_code.code_challenge and self.code_verifier:
|
if not self.authorization_code.code_challenge and self.code_verifier:
|
||||||
raise TokenError("invalid_grant")
|
raise TokenError("invalid_grant")
|
||||||
|
|
||||||
|
def __check_redirect_uri(self, request: HttpRequest):
|
||||||
|
allowed_redirect_urls = self.provider.redirect_uris
|
||||||
|
# At this point, no provider should have a blank redirect_uri, in case they do
|
||||||
|
# this will check an empty array and raise an error
|
||||||
|
|
||||||
|
match_found = False
|
||||||
|
for allowed in allowed_redirect_urls:
|
||||||
|
if allowed.matching_mode == RedirectURIMatchingMode.STRICT:
|
||||||
|
if self.redirect_uri == allowed.url:
|
||||||
|
match_found = True
|
||||||
|
break
|
||||||
|
if allowed.matching_mode == RedirectURIMatchingMode.REGEX:
|
||||||
|
try:
|
||||||
|
if fullmatch(allowed.url, self.redirect_uri):
|
||||||
|
match_found = True
|
||||||
|
break
|
||||||
|
except RegexError as exc:
|
||||||
|
LOGGER.warning(
|
||||||
|
"Failed to parse regular expression",
|
||||||
|
exc=exc,
|
||||||
|
url=allowed.url,
|
||||||
|
provider=self.provider,
|
||||||
|
)
|
||||||
|
Event.new(
|
||||||
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
message="Invalid redirect_uri configured",
|
||||||
|
provider=self.provider,
|
||||||
|
).from_http(request)
|
||||||
|
if not match_found:
|
||||||
|
Event.new(
|
||||||
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
message="Invalid redirect URI used by provider",
|
||||||
|
provider=self.provider,
|
||||||
|
redirect_uri=self.redirect_uri,
|
||||||
|
expected=allowed_redirect_urls,
|
||||||
|
).from_http(request)
|
||||||
|
raise TokenError("invalid_client")
|
||||||
|
|
||||||
|
# Check against forbidden schemes
|
||||||
|
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||||
|
raise TokenError("invalid_request")
|
||||||
|
|
||||||
def __post_init_refresh(self, raw_token: str, request: HttpRequest):
|
def __post_init_refresh(self, raw_token: str, request: HttpRequest):
|
||||||
if not raw_token:
|
if not raw_token:
|
||||||
LOGGER.warning("Missing refresh token")
|
LOGGER.warning("Missing refresh token")
|
||||||
@ -497,7 +521,7 @@ class TokenView(View):
|
|||||||
response = super().dispatch(request, *args, **kwargs)
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
allowed_origins = []
|
allowed_origins = []
|
||||||
if self.provider:
|
if self.provider:
|
||||||
allowed_origins = self.provider.redirect_uris.split("\n")
|
allowed_origins = [x.url for x in self.provider.redirect_uris]
|
||||||
cors_allow(self.request, response, *allowed_origins)
|
cors_allow(self.request, response, *allowed_origins)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -710,7 +734,7 @@ class TokenView(View):
|
|||||||
"id_token": access_token.id_token.to_jwt(self.provider),
|
"id_token": access_token.id_token.to_jwt(self.provider),
|
||||||
}
|
}
|
||||||
|
|
||||||
if SCOPE_OFFLINE_ACCESS in self.params.scope:
|
if SCOPE_OFFLINE_ACCESS in self.params.device_code.scope:
|
||||||
refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity)
|
refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity)
|
||||||
refresh_token = RefreshToken(
|
refresh_token = RefreshToken(
|
||||||
user=self.params.device_code.user,
|
user=self.params.device_code.user,
|
||||||
|
|||||||
@ -108,7 +108,7 @@ class UserInfoView(View):
|
|||||||
response = super().dispatch(request, *args, **kwargs)
|
response = super().dispatch(request, *args, **kwargs)
|
||||||
allowed_origins = []
|
allowed_origins = []
|
||||||
if self.token:
|
if self.token:
|
||||||
allowed_origins = self.token.provider.redirect_uris.split("\n")
|
allowed_origins = [x.url for x in self.token.provider.redirect_uris]
|
||||||
cors_allow(self.request, response, *allowed_origins)
|
cors_allow(self.request, response, *allowed_origins)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from authentik.core.api.providers import ProviderSerializer
|
|||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
|
from authentik.providers.oauth2.api.providers import RedirectURISerializer
|
||||||
from authentik.providers.oauth2.models import ScopeMapping
|
from authentik.providers.oauth2.models import ScopeMapping
|
||||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||||
@ -39,7 +40,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
|||||||
"""ProxyProvider Serializer"""
|
"""ProxyProvider Serializer"""
|
||||||
|
|
||||||
client_id = CharField(read_only=True)
|
client_id = CharField(read_only=True)
|
||||||
redirect_uris = CharField(read_only=True)
|
redirect_uris = RedirectURISerializer(many=True, read_only=True, source="_redirect_uris")
|
||||||
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
||||||
|
|
||||||
def validate_basic_auth_enabled(self, value: bool) -> bool:
|
def validate_basic_auth_enabled(self, value: bool) -> bool:
|
||||||
@ -121,7 +122,6 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"basic_auth_password_attribute": ["iexact"],
|
"basic_auth_password_attribute": ["iexact"],
|
||||||
"basic_auth_user_attribute": ["iexact"],
|
"basic_auth_user_attribute": ["iexact"],
|
||||||
"mode": ["iexact"],
|
"mode": ["iexact"],
|
||||||
"redirect_uris": ["iexact"],
|
|
||||||
"cookie_domain": ["iexact"],
|
"cookie_domain": ["iexact"],
|
||||||
}
|
}
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
|
|||||||
@ -13,7 +13,13 @@ from rest_framework.serializers import Serializer
|
|||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.models import DomainlessURLValidator
|
from authentik.lib.models import DomainlessURLValidator
|
||||||
from authentik.outposts.models import OutpostModel
|
from authentik.outposts.models import OutpostModel
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
ClientTypes,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
|
|
||||||
SCOPE_AK_PROXY = "ak_proxy"
|
SCOPE_AK_PROXY = "ak_proxy"
|
||||||
OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"
|
OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"
|
||||||
@ -24,14 +30,14 @@ def get_cookie_secret():
|
|||||||
return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||||||
|
|
||||||
|
|
||||||
def _get_callback_url(uri: str) -> str:
|
def _get_callback_url(uri: str) -> list[RedirectURI]:
|
||||||
return "\n".join(
|
return [
|
||||||
[
|
RedirectURI(
|
||||||
urljoin(uri, "outpost.goauthentik.io/callback")
|
RedirectURIMatchingMode.STRICT,
|
||||||
+ f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
urljoin(uri, "outpost.goauthentik.io/callback") + f"?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
||||||
uri + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
),
|
||||||
]
|
RedirectURI(RedirectURIMatchingMode.STRICT, uri + f"?{OUTPOST_CALLBACK_SIGNATURE}=true"),
|
||||||
)
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProxyMode(models.TextChoices):
|
class ProxyMode(models.TextChoices):
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
"""Metrics view"""
|
"""Metrics view"""
|
||||||
|
|
||||||
from base64 import b64encode
|
from hmac import compare_digest
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import gettempdir
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import connections
|
from django.db import connections
|
||||||
@ -16,22 +18,21 @@ monitoring_set = Signal()
|
|||||||
|
|
||||||
|
|
||||||
class MetricsView(View):
|
class MetricsView(View):
|
||||||
"""Wrapper around ExportToDjangoView, using http-basic auth"""
|
"""Wrapper around ExportToDjangoView with authentication, accessed by the authentik router"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
_tmp = Path(gettempdir())
|
||||||
|
with open(_tmp / "authentik-core-metrics.key") as _f:
|
||||||
|
self.monitoring_key = _f.read()
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""Check for HTTP-Basic auth"""
|
"""Check for HTTP-Basic auth"""
|
||||||
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
|
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
|
||||||
auth_type, _, given_credentials = auth_header.partition(" ")
|
auth_type, _, given_credentials = auth_header.partition(" ")
|
||||||
credentials = f"monitor:{settings.SECRET_KEY}"
|
authed = auth_type == "Bearer" and compare_digest(given_credentials, self.monitoring_key)
|
||||||
expected = b64encode(str.encode(credentials)).decode()
|
|
||||||
authed = auth_type == "Basic" and given_credentials == expected
|
|
||||||
if not authed and not settings.DEBUG:
|
if not authed and not settings.DEBUG:
|
||||||
response = HttpResponse(status=401)
|
return HttpResponse(status=401)
|
||||||
response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"'
|
|
||||||
return response
|
|
||||||
|
|
||||||
monitoring_set.send_robust(self)
|
monitoring_set.send_robust(self)
|
||||||
|
|
||||||
return ExportToDjangoView(request)
|
return ExportToDjangoView(request)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
"""root tests"""
|
"""root tests"""
|
||||||
|
|
||||||
from base64 import b64encode
|
from pathlib import Path
|
||||||
|
from secrets import token_urlsafe
|
||||||
|
from tempfile import gettempdir
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@ -10,6 +11,16 @@ from django.urls import reverse
|
|||||||
class TestRoot(TestCase):
|
class TestRoot(TestCase):
|
||||||
"""Test root application"""
|
"""Test root application"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
_tmp = Path(gettempdir())
|
||||||
|
self.token = token_urlsafe(32)
|
||||||
|
with open(_tmp / "authentik-core-metrics.key", "w") as _f:
|
||||||
|
_f.write(self.token)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
_tmp = Path(gettempdir())
|
||||||
|
(_tmp / "authentik-core-metrics.key").unlink()
|
||||||
|
|
||||||
def test_monitoring_error(self):
|
def test_monitoring_error(self):
|
||||||
"""Test monitoring without any credentials"""
|
"""Test monitoring without any credentials"""
|
||||||
response = self.client.get(reverse("metrics"))
|
response = self.client.get(reverse("metrics"))
|
||||||
@ -17,8 +28,7 @@ class TestRoot(TestCase):
|
|||||||
|
|
||||||
def test_monitoring_ok(self):
|
def test_monitoring_ok(self):
|
||||||
"""Test monitoring with credentials"""
|
"""Test monitoring with credentials"""
|
||||||
creds = "Basic " + b64encode(f"monitor:{settings.SECRET_KEY}".encode()).decode("utf-8")
|
auth_headers = {"HTTP_AUTHORIZATION": f"Bearer {self.token}"}
|
||||||
auth_headers = {"HTTP_AUTHORIZATION": creds}
|
|
||||||
response = self.client.get(reverse("metrics"), **auth_headers)
|
response = self.client.get(reverse("metrics"), **auth_headers)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|||||||
@ -5570,9 +5570,30 @@
|
|||||||
"description": "Key used to encrypt the tokens. When set, tokens will be encrypted and returned as JWEs."
|
"description": "Key used to encrypt the tokens. When set, tokens will be encrypted and returned as JWEs."
|
||||||
},
|
},
|
||||||
"redirect_uris": {
|
"redirect_uris": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"title": "Redirect URIs",
|
"items": {
|
||||||
"description": "Enter each URI on a new line."
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"matching_mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"strict",
|
||||||
|
"regex"
|
||||||
|
],
|
||||||
|
"title": "Matching mode"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Url"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"matching_mode",
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"title": "Redirect uris"
|
||||||
},
|
},
|
||||||
"sub_mode": {
|
"sub_mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@ -65,7 +65,7 @@ func (ls *LDAPServer) StartLDAPServer() error {
|
|||||||
ls.log.WithField("listen", listen).WithError(err).Warning("Failed to listen (SSL)")
|
ls.log.WithField("listen", listen).WithError(err).Warning("Failed to listen (SSL)")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
ls.log.WithField("listen", listen).Info("Starting LDAP server")
|
ls.log.WithField("listen", listen).Info("Starting LDAP server")
|
||||||
|
|||||||
@ -48,7 +48,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
tln := tls.NewListener(proxyListener, tlsConfig)
|
tln := tls.NewListener(proxyListener, tlsConfig)
|
||||||
|
|||||||
@ -129,7 +129,7 @@ func (ps *ProxyServer) ServeHTTP() {
|
|||||||
ps.log.WithField("listen", listenAddress).WithError(err).Warning("Failed to listen")
|
ps.log.WithField("listen", listenAddress).WithError(err).Warning("Failed to listen")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: listener}
|
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
|
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
|
||||||
@ -148,7 +148,7 @@ func (ps *ProxyServer) ServeHTTPS() {
|
|||||||
ps.log.WithError(err).Warning("Failed to listen (TLS)")
|
ps.log.WithError(err).Warning("Failed to listen (TLS)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}}
|
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||||
|
|||||||
34
internal/utils/proxy.go
Normal file
34
internal/utils/proxy.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/pires/go-proxyproto"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetProxyConnectionPolicy() proxyproto.ConnPolicyFunc {
|
||||||
|
nets := []*net.IPNet{}
|
||||||
|
for _, rn := range config.Get().Listen.TrustedProxyCIDRs {
|
||||||
|
_, cidr, err := net.ParseCIDR(rn)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nets = append(nets, cidr)
|
||||||
|
}
|
||||||
|
return func(connPolicyOptions proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
|
||||||
|
host, _, err := net.SplitHostPort(connPolicyOptions.Upstream.String())
|
||||||
|
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("Using remote IP from proxy protocol")
|
||||||
|
return proxyproto.USE, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return proxyproto.SKIP, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,15 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
@ -14,14 +18,25 @@ import (
|
|||||||
"goauthentik.io/internal/utils/sentry"
|
"goauthentik.io/internal/utils/sentry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MetricsKeyFile = "authentik-core-metrics.key"
|
||||||
|
|
||||||
var Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
var Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
Name: "authentik_main_request_duration_seconds",
|
Name: "authentik_main_request_duration_seconds",
|
||||||
Help: "API request latencies in seconds",
|
Help: "API request latencies in seconds",
|
||||||
}, []string{"dest"})
|
}, []string{"dest"})
|
||||||
|
|
||||||
func (ws *WebServer) runMetricsServer() {
|
func (ws *WebServer) runMetricsServer() {
|
||||||
m := mux.NewRouter()
|
|
||||||
l := log.WithField("logger", "authentik.router.metrics")
|
l := log.WithField("logger", "authentik.router.metrics")
|
||||||
|
tmp := os.TempDir()
|
||||||
|
key := base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(64))
|
||||||
|
keyPath := path.Join(tmp, MetricsKeyFile)
|
||||||
|
err := os.WriteFile(keyPath, []byte(key), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
l.WithError(err).Warning("failed to save metrics key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m := mux.NewRouter()
|
||||||
m.Use(sentry.SentryNoSampleMiddleware)
|
m.Use(sentry.SentryNoSampleMiddleware)
|
||||||
m.Path("/metrics").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
m.Path("/metrics").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
promhttp.InstrumentMetricHandler(
|
promhttp.InstrumentMetricHandler(
|
||||||
@ -36,7 +51,7 @@ func (ws *WebServer) runMetricsServer() {
|
|||||||
l.WithError(err).Warning("failed to get upstream metrics")
|
l.WithError(err).Warning("failed to get upstream metrics")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
re.SetBasicAuth("monitor", config.Get().SecretKey)
|
re.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
|
||||||
res, err := ws.upstreamHttpClient().Do(re)
|
res, err := ws.upstreamHttpClient().Do(re)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Warning("failed to get upstream metrics")
|
l.WithError(err).Warning("failed to get upstream metrics")
|
||||||
@ -49,9 +64,13 @@ func (ws *WebServer) runMetricsServer() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
l.WithField("listen", config.Get().Listen.Metrics).Info("Starting Metrics server")
|
l.WithField("listen", config.Get().Listen.Metrics).Info("Starting Metrics server")
|
||||||
err := http.ListenAndServe(config.Get().Listen.Metrics, m)
|
err = http.ListenAndServe(config.Get().Listen.Metrics, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Warning("Failed to start metrics server")
|
l.WithError(err).Warning("Failed to start metrics server")
|
||||||
}
|
}
|
||||||
l.WithField("listen", config.Get().Listen.Metrics).Info("Stopping Metrics server")
|
l.WithField("listen", config.Get().Listen.Metrics).Info("Stopping Metrics server")
|
||||||
|
err = os.Remove(keyPath)
|
||||||
|
if err != nil {
|
||||||
|
l.WithError(err).Warning("failed to remove metrics key file")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,8 +42,11 @@ func (ws *WebServer) configureStatic() {
|
|||||||
|
|
||||||
// Media files, if backend is file
|
// Media files, if backend is file
|
||||||
if config.Get().Storage.Media.Backend == "file" {
|
if config.Get().Storage.Media.Backend == "file" {
|
||||||
fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))
|
fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path)))
|
||||||
staticRouter.PathPrefix("/media/").Handler(http.StripPrefix("/media", fsMedia))
|
staticRouter.PathPrefix("/media/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||||
|
fsMedia.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
staticRouter.PathPrefix("/if/help/").Handler(http.StripPrefix("/if/help/", http.FileServer(http.Dir("./website/help/"))))
|
staticRouter.PathPrefix("/if/help/").Handler(http.StripPrefix("/if/help/", http.FileServer(http.Dir("./website/help/"))))
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import (
|
|||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
"goauthentik.io/internal/gounicorn"
|
"goauthentik.io/internal/gounicorn"
|
||||||
"goauthentik.io/internal/outpost/proxyv2"
|
"goauthentik.io/internal/outpost/proxyv2"
|
||||||
|
"goauthentik.io/internal/utils"
|
||||||
"goauthentik.io/internal/utils/web"
|
"goauthentik.io/internal/utils/web"
|
||||||
"goauthentik.io/internal/web/brand_tls"
|
"goauthentik.io/internal/web/brand_tls"
|
||||||
)
|
)
|
||||||
@ -52,7 +53,7 @@ func NewWebServer() *WebServer {
|
|||||||
loggingHandler.Use(web.NewLoggingHandler(l, nil))
|
loggingHandler.Use(web.NewLoggingHandler(l, nil))
|
||||||
|
|
||||||
tmp := os.TempDir()
|
tmp := os.TempDir()
|
||||||
socketPath := path.Join(tmp, "authentik-core.sock")
|
socketPath := path.Join(tmp, UnixSocketName)
|
||||||
|
|
||||||
// create http client to talk to backend, normal client if we're in debug more
|
// create http client to talk to backend, normal client if we're in debug more
|
||||||
// and a client that connects to our socket when in non debug mode
|
// and a client that connects to our socket when in non debug mode
|
||||||
@ -149,7 +150,7 @@ func (ws *WebServer) listenPlain() {
|
|||||||
ws.log.WithError(err).Warning("failed to listen")
|
ws.log.WithError(err).Warning("failed to listen")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
|
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
|
||||||
|
|||||||
@ -45,7 +45,7 @@ func (ws *WebServer) listenTLS() {
|
|||||||
ws.log.WithError(err).Warning("failed to listen (TLS)")
|
ws.log.WithError(err).Warning("failed to listen (TLS)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}}
|
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||||
@ -19,7 +19,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-10-28 00:09+0000\n"
|
"POT-Creation-Date: 2024-11-18 00:09+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: tom max, 2024\n"
|
"Last-Translator: tom max, 2024\n"
|
||||||
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
||||||
@ -121,6 +121,10 @@ msgstr "Brand"
|
|||||||
msgid "Brands"
|
msgid "Brands"
|
||||||
msgstr "Brands"
|
msgstr "Brands"
|
||||||
|
|
||||||
|
#: authentik/core/api/devices.py
|
||||||
|
msgid "Extra description not available"
|
||||||
|
msgstr "Descrizione extra non disponibile"
|
||||||
|
|
||||||
#: authentik/core/api/providers.py
|
#: authentik/core/api/providers.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"When not set all providers are returned. When set to true, only backchannel "
|
"When not set all providers are returned. When set to true, only backchannel "
|
||||||
@ -131,6 +135,11 @@ msgstr ""
|
|||||||
" vengono restituiti solo i provider di backchannel. Se impostato su falso, i"
|
" vengono restituiti solo i provider di backchannel. Se impostato su falso, i"
|
||||||
" provider di backchannel vengono esclusi"
|
" provider di backchannel vengono esclusi"
|
||||||
|
|
||||||
|
#: authentik/core/api/transactional_applications.py
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "User lacks permission to create {model}"
|
||||||
|
msgstr "L'utente non ha i diritti per creare {model}"
|
||||||
|
|
||||||
#: authentik/core/api/users.py
|
#: authentik/core/api/users.py
|
||||||
msgid "No leading or trailing slashes allowed."
|
msgid "No leading or trailing slashes allowed."
|
||||||
msgstr "Non sono consentite barre oblique iniziali o finali."
|
msgstr "Non sono consentite barre oblique iniziali o finali."
|
||||||
@ -1240,6 +1249,10 @@ msgstr ""
|
|||||||
msgid "Password not set in context"
|
msgid "Password not set in context"
|
||||||
msgstr "Password non impostata nel contesto"
|
msgstr "Password non impostata nel contesto"
|
||||||
|
|
||||||
|
#: authentik/policies/password/models.py
|
||||||
|
msgid "Invalid password."
|
||||||
|
msgstr "Password invalida."
|
||||||
|
|
||||||
#: authentik/policies/password/models.py
|
#: authentik/policies/password/models.py
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Password exists on %(count)d online lists."
|
msgid "Password exists on %(count)d online lists."
|
||||||
@ -3550,6 +3563,12 @@ msgstr ""
|
|||||||
msgid "Globally enable/disable impersonation."
|
msgid "Globally enable/disable impersonation."
|
||||||
msgstr "Abilita/disabilita globalmente la l'impersonazione."
|
msgstr "Abilita/disabilita globalmente la l'impersonazione."
|
||||||
|
|
||||||
|
#: authentik/tenants/models.py
|
||||||
|
msgid "Require administrators to provide a reason for impersonating a user."
|
||||||
|
msgstr ""
|
||||||
|
"Richiedi agli amministratori di fornire una ragione per impersonare un "
|
||||||
|
"utente."
|
||||||
|
|
||||||
#: authentik/tenants/models.py
|
#: authentik/tenants/models.py
|
||||||
msgid "Default token duration"
|
msgid "Default token duration"
|
||||||
msgstr "Durata token predefinita"
|
msgstr "Durata token predefinita"
|
||||||
|
|||||||
300
poetry.lock
generated
300
poetry.lock
generated
@ -1790,13 +1790,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-api-python-client"
|
name = "google-api-python-client"
|
||||||
version = "2.153.0"
|
version = "2.154.0"
|
||||||
description = "Google API Client Library for Python"
|
description = "Google API Client Library for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "google_api_python_client-2.153.0-py2.py3-none-any.whl", hash = "sha256:6ff13bbfa92a57972e33ec3808e18309e5981b8ca1300e5da23bf2b4d6947384"},
|
{file = "google_api_python_client-2.154.0-py2.py3-none-any.whl", hash = "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad"},
|
||||||
{file = "google_api_python_client-2.153.0.tar.gz", hash = "sha256:35cce8647f9c163fc04fb4d811fc91aae51954a2bdd74918decbe0e65d791dd2"},
|
{file = "google_api_python_client-2.154.0.tar.gz", hash = "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -1993,51 +1993,58 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httptools"
|
name = "httptools"
|
||||||
version = "0.6.1"
|
version = "0.6.4"
|
||||||
description = "A collection of framework independent HTTP protocol utils."
|
description = "A collection of framework independent HTTP protocol utils."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"},
|
{file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"},
|
||||||
{file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"},
|
{file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"},
|
||||||
{file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"},
|
{file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"},
|
||||||
{file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"},
|
{file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"},
|
||||||
{file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"},
|
{file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"},
|
||||||
{file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"},
|
{file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"},
|
||||||
{file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"},
|
{file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"},
|
||||||
{file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"},
|
{file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"},
|
||||||
{file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"},
|
{file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"},
|
||||||
{file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"},
|
{file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"},
|
||||||
{file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"},
|
{file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"},
|
||||||
{file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"},
|
{file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"},
|
||||||
{file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"},
|
{file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"},
|
||||||
{file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"},
|
{file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"},
|
||||||
{file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"},
|
{file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"},
|
||||||
{file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"},
|
{file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"},
|
||||||
{file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"},
|
{file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"},
|
||||||
{file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"},
|
{file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"},
|
||||||
{file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"},
|
{file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"},
|
||||||
{file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"},
|
{file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"},
|
||||||
{file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"},
|
{file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"},
|
||||||
{file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"},
|
{file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"},
|
||||||
{file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"},
|
{file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"},
|
||||||
{file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"},
|
{file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"},
|
||||||
{file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"},
|
{file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"},
|
||||||
{file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"},
|
{file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"},
|
||||||
{file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"},
|
{file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"},
|
||||||
{file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"},
|
{file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"},
|
||||||
{file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"},
|
{file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"},
|
||||||
{file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"},
|
{file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"},
|
||||||
{file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"},
|
{file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"},
|
||||||
{file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"},
|
{file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"},
|
||||||
{file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"},
|
{file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"},
|
||||||
{file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"},
|
{file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"},
|
||||||
{file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"},
|
{file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"},
|
||||||
{file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"},
|
{file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"},
|
||||||
|
{file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"},
|
||||||
|
{file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"},
|
||||||
|
{file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"},
|
||||||
|
{file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"},
|
||||||
|
{file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"},
|
||||||
|
{file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"},
|
||||||
|
{file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
test = ["Cython (>=0.29.24,<0.30.0)"]
|
test = ["Cython (>=0.29.24)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
@ -3711,20 +3718,20 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.9.2"
|
version = "2.10.0"
|
||||||
description = "Data validation using Python type hints"
|
description = "Data validation using Python type hints"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
|
{file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"},
|
||||||
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
|
{file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
annotated-types = ">=0.6.0"
|
annotated-types = ">=0.6.0"
|
||||||
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
|
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
|
||||||
pydantic-core = "2.23.4"
|
pydantic-core = "2.27.0"
|
||||||
typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
|
typing-extensions = ">=4.12.2"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
email = ["email-validator (>=2.0.0)"]
|
email = ["email-validator (>=2.0.0)"]
|
||||||
@ -3732,100 +3739,111 @@ timezone = ["tzdata"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-core"
|
name = "pydantic-core"
|
||||||
version = "2.23.4"
|
version = "2.27.0"
|
||||||
description = "Core functionality for Pydantic validation and serialization"
|
description = "Core functionality for Pydantic validation and serialization"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
|
{file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373"},
|
||||||
{file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
|
{file = "pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
|
{file = "pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"},
|
||||||
{file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
|
{file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
|
{file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
|
{file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
|
{file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"},
|
||||||
{file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
|
{file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
|
{file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
|
{file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
|
{file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"},
|
||||||
{file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"},
|
{file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"},
|
{file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"},
|
{file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"},
|
{file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96"},
|
||||||
{file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"},
|
{file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"},
|
{file = "pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"},
|
{file = "pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9"},
|
||||||
{file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7"},
|
||||||
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae"},
|
||||||
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12"},
|
||||||
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636"},
|
||||||
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196"},
|
||||||
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb"},
|
||||||
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90"},
|
||||||
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
|
{file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd"},
|
||||||
{file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
|
{file = "pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846"},
|
||||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"},
|
{file = "pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361"},
|
||||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"},
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c"},
|
||||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"},
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa"},
|
||||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"},
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb"},
|
||||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"},
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a"},
|
||||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"},
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df"},
|
||||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"},
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3"},
|
||||||
{file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d"},
|
||||||
{file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3"},
|
||||||
|
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739"},
|
||||||
|
{file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -5041,20 +5059,20 @@ zstd = ["zstandard (>=0.18.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.32.0"
|
version = "0.32.1"
|
||||||
description = "The lightning-fast ASGI server."
|
description = "The lightning-fast ASGI server."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82"},
|
{file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"},
|
||||||
{file = "uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e"},
|
{file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
click = ">=7.0"
|
click = ">=7.0"
|
||||||
colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""}
|
colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""}
|
||||||
h11 = ">=0.8"
|
h11 = ">=0.8"
|
||||||
httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""}
|
httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""}
|
||||||
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
|
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
|
||||||
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
|
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
|
||||||
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
|
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
|
||||||
@ -5062,7 +5080,7 @@ watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standar
|
|||||||
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
|
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvloop"
|
name = "uvloop"
|
||||||
|
|||||||
57
schema.yml
57
schema.yml
@ -20224,10 +20224,6 @@ paths:
|
|||||||
format: uuid
|
format: uuid
|
||||||
explode: true
|
explode: true
|
||||||
style: form
|
style: form
|
||||||
- in: query
|
|
||||||
name: redirect_uris
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- in: query
|
- in: query
|
||||||
name: refresh_token_validity
|
name: refresh_token_validity
|
||||||
schema:
|
schema:
|
||||||
@ -20643,10 +20639,6 @@ paths:
|
|||||||
format: uuid
|
format: uuid
|
||||||
explode: true
|
explode: true
|
||||||
style: form
|
style: form
|
||||||
- in: query
|
|
||||||
name: redirect_uris__iexact
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: search
|
- name: search
|
||||||
required: false
|
required: false
|
||||||
in: query
|
in: query
|
||||||
@ -44074,6 +44066,11 @@ components:
|
|||||||
required:
|
required:
|
||||||
- challenge
|
- challenge
|
||||||
- name
|
- name
|
||||||
|
MatchingModeEnum:
|
||||||
|
enum:
|
||||||
|
- strict
|
||||||
|
- regex
|
||||||
|
type: string
|
||||||
Metadata:
|
Metadata:
|
||||||
type: object
|
type: object
|
||||||
description: Serializer for blueprint metadata
|
description: Serializer for blueprint metadata
|
||||||
@ -44776,8 +44773,9 @@ components:
|
|||||||
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
||||||
and returned as JWEs.
|
and returned as JWEs.
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
type: string
|
type: array
|
||||||
description: Enter each URI on a new line.
|
items:
|
||||||
|
$ref: '#/components/schemas/RedirectURI'
|
||||||
sub_mode:
|
sub_mode:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/SubModeEnum'
|
- $ref: '#/components/schemas/SubModeEnum'
|
||||||
@ -44806,6 +44804,7 @@ components:
|
|||||||
- meta_model_name
|
- meta_model_name
|
||||||
- name
|
- name
|
||||||
- pk
|
- pk
|
||||||
|
- redirect_uris
|
||||||
- verbose_name
|
- verbose_name
|
||||||
- verbose_name_plural
|
- verbose_name_plural
|
||||||
OAuth2ProviderRequest:
|
OAuth2ProviderRequest:
|
||||||
@ -44877,8 +44876,9 @@ components:
|
|||||||
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
||||||
and returned as JWEs.
|
and returned as JWEs.
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
type: string
|
type: array
|
||||||
description: Enter each URI on a new line.
|
items:
|
||||||
|
$ref: '#/components/schemas/RedirectURIRequest'
|
||||||
sub_mode:
|
sub_mode:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/SubModeEnum'
|
- $ref: '#/components/schemas/SubModeEnum'
|
||||||
@ -44900,6 +44900,7 @@ components:
|
|||||||
- authorization_flow
|
- authorization_flow
|
||||||
- invalidation_flow
|
- invalidation_flow
|
||||||
- name
|
- name
|
||||||
|
- redirect_uris
|
||||||
OAuth2ProviderSetupURLs:
|
OAuth2ProviderSetupURLs:
|
||||||
type: object
|
type: object
|
||||||
description: OAuth2 Provider Metadata serializer
|
description: OAuth2 Provider Metadata serializer
|
||||||
@ -48898,8 +48899,9 @@ components:
|
|||||||
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
||||||
and returned as JWEs.
|
and returned as JWEs.
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
type: string
|
type: array
|
||||||
description: Enter each URI on a new line.
|
items:
|
||||||
|
$ref: '#/components/schemas/RedirectURIRequest'
|
||||||
sub_mode:
|
sub_mode:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/SubModeEnum'
|
- $ref: '#/components/schemas/SubModeEnum'
|
||||||
@ -51496,7 +51498,9 @@ components:
|
|||||||
description: When enabled, this provider will intercept the authorization
|
description: When enabled, this provider will intercept the authorization
|
||||||
header and authenticate requests based on its value.
|
header and authenticate requests based on its value.
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
type: string
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RedirectURI'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
cookie_domain:
|
cookie_domain:
|
||||||
type: string
|
type: string
|
||||||
@ -52092,6 +52096,29 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- to
|
- to
|
||||||
|
RedirectURI:
|
||||||
|
type: object
|
||||||
|
description: A single allowed redirect URI entry
|
||||||
|
properties:
|
||||||
|
matching_mode:
|
||||||
|
$ref: '#/components/schemas/MatchingModeEnum'
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- matching_mode
|
||||||
|
- url
|
||||||
|
RedirectURIRequest:
|
||||||
|
type: object
|
||||||
|
description: A single allowed redirect URI entry
|
||||||
|
properties:
|
||||||
|
matching_mode:
|
||||||
|
$ref: '#/components/schemas/MatchingModeEnum'
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
required:
|
||||||
|
- matching_mode
|
||||||
|
- url
|
||||||
Reputation:
|
Reputation:
|
||||||
type: object
|
type: object
|
||||||
description: Reputation Serializer
|
description: Reputation Serializer
|
||||||
|
|||||||
@ -12,7 +12,12 @@ from authentik.flows.models import Flow
|
|||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider
|
from authentik.providers.oauth2.models import (
|
||||||
|
ClientTypes,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
)
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@ -73,7 +78,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
redirect_uris="http://localhost:3000/login/github",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github")
|
||||||
|
],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
Application.objects.create(
|
Application.objects.create(
|
||||||
@ -128,7 +135,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
redirect_uris="http://localhost:3000/login/github",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github")
|
||||||
|
],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
app = Application.objects.create(
|
app = Application.objects.create(
|
||||||
@ -199,7 +208,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
redirect_uris="http://localhost:3000/login/github",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github")
|
||||||
|
],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
app = Application.objects.create(
|
app = Application.objects.create(
|
||||||
|
|||||||
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID_EMAIL,
|
SCOPE_OPENID_EMAIL,
|
||||||
SCOPE_OPENID_PROFILE,
|
SCOPE_OPENID_PROFILE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
ClientTypes,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@ -82,7 +88,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:3000/",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/")],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -131,7 +137,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
redirect_uris=[
|
||||||
|
RedirectURI(
|
||||||
|
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
|
||||||
|
)
|
||||||
|
],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -200,7 +210,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
redirect_uris=[
|
||||||
|
RedirectURI(
|
||||||
|
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
|
||||||
|
)
|
||||||
|
],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
invalidation_flow=invalidation_flow,
|
invalidation_flow=invalidation_flow,
|
||||||
)
|
)
|
||||||
@ -275,7 +289,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
redirect_uris=[
|
||||||
|
RedirectURI(
|
||||||
|
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -355,7 +373,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
redirect_uris=[
|
||||||
|
RedirectURI(
|
||||||
|
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
|
|||||||
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID_EMAIL,
|
SCOPE_OPENID_EMAIL,
|
||||||
SCOPE_OPENID_PROFILE,
|
SCOPE_OPENID_PROFILE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
ClientTypes,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@ -67,7 +73,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -116,7 +122,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/auth/callback",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
|
||||||
|
],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -188,7 +196,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/auth/callback",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
|
||||||
|
],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -259,7 +269,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/auth/callback",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
|
||||||
|
],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
|
|||||||
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
|||||||
SCOPE_OPENID_EMAIL,
|
SCOPE_OPENID_EMAIL,
|
||||||
SCOPE_OPENID_PROFILE,
|
SCOPE_OPENID_PROFILE,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import (
|
||||||
|
ClientTypes,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.e2e.utils import SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@ -68,7 +74,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/",
|
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -117,7 +123,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/implicit/",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||||
|
],
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
@ -170,7 +178,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/implicit/",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||||
|
],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -238,7 +248,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
|||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
redirect_uris="http://localhost:9009/implicit/",
|
redirect_uris=[
|
||||||
|
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||||
|
],
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
|
|||||||
16
web/package-lock.json
generated
16
web/package-lock.json
generated
@ -23,7 +23,7 @@
|
|||||||
"@floating-ui/dom": "^1.6.11",
|
"@floating-ui/dom": "^1.6.11",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@goauthentik/api": "^2024.10.2-1731887740",
|
"@goauthentik/api": "^2024.10.2-1732206118",
|
||||||
"@lit-labs/ssr": "^3.2.2",
|
"@lit-labs/ssr": "^3.2.2",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.2",
|
"@lit/localize": "^0.12.2",
|
||||||
@ -84,7 +84,7 @@
|
|||||||
"@wdio/cli": "^9.1.2",
|
"@wdio/cli": "^9.1.2",
|
||||||
"@wdio/spec-reporter": "^9.1.2",
|
"@wdio/spec-reporter": "^9.1.2",
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"chromedriver": "^129.0.2",
|
"chromedriver": "^130.0.4",
|
||||||
"esbuild": "^0.24.0",
|
"esbuild": "^0.24.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-lit": "^1.15.0",
|
"eslint-plugin-lit": "^1.15.0",
|
||||||
@ -1775,9 +1775,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/api": {
|
"node_modules/@goauthentik/api": {
|
||||||
"version": "2024.10.2-1731887740",
|
"version": "2024.10.2-1732206118",
|
||||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.10.2-1731887740.tgz",
|
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.10.2-1732206118.tgz",
|
||||||
"integrity": "sha512-AYFgmLXPvwLsxMdCX0e51FD2Fj8T++L8beU3dyDvlWUp4jH5+2XMG/AtQI3v2mNCyKx1EDlublIVYzpmQnrtag=="
|
"integrity": "sha512-Zg90AJvGDquD3u73yIBKXFBDxsCljPxVqylylS6hgPzkLSogKVVkjhmKteWFXDrVxxsxo5XIa4FuTe3wAERyzw=="
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/web": {
|
"node_modules/@goauthentik/web": {
|
||||||
"resolved": "",
|
"resolved": "",
|
||||||
@ -8699,9 +8699,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chromedriver": {
|
"node_modules/chromedriver": {
|
||||||
"version": "129.0.2",
|
"version": "130.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-129.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-130.0.4.tgz",
|
||||||
"integrity": "sha512-rUEFCJAmAwOdFfaDFtveT97fFeA7NOxlkgyPyN+G09Ws4qGW39aLDxMQBbS9cxQQHhTihqZZobgF5CLVYXnmGA==",
|
"integrity": "sha512-lpR+PWXszij1k4Ig3t338Zvll9HtCTiwoLM7n4pCCswALHxzmgwaaIFBh3rt9+5wRk9D07oFblrazrBxwaYYAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"@floating-ui/dom": "^1.6.11",
|
"@floating-ui/dom": "^1.6.11",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@goauthentik/api": "^2024.10.2-1731887740",
|
"@goauthentik/api": "^2024.10.2-1732206118",
|
||||||
"@lit-labs/ssr": "^3.2.2",
|
"@lit-labs/ssr": "^3.2.2",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.2",
|
"@lit/localize": "^0.12.2",
|
||||||
@ -72,7 +72,7 @@
|
|||||||
"@wdio/cli": "^9.1.2",
|
"@wdio/cli": "^9.1.2",
|
||||||
"@wdio/spec-reporter": "^9.1.2",
|
"@wdio/spec-reporter": "^9.1.2",
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"chromedriver": "^129.0.2",
|
"chromedriver": "^130.0.4",
|
||||||
"esbuild": "^0.24.0",
|
"esbuild": "^0.24.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-lit": "^1.15.0",
|
"eslint-plugin-lit": "^1.15.0",
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import {
|
|||||||
type TransactionApplicationRequest,
|
type TransactionApplicationRequest,
|
||||||
type TransactionApplicationResponse,
|
type TransactionApplicationResponse,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
|
instanceOfValidationError,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
import BasePanel from "../BasePanel";
|
import BasePanel from "../BasePanel";
|
||||||
@ -69,6 +70,9 @@ const successState: State = {
|
|||||||
icon: ["fa-check-circle", "pf-m-success"],
|
icon: ["fa-check-circle", "pf-m-success"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const isValidationError = (v: any): v is ValidationError => instanceOfValidationError(v);
|
||||||
|
|
||||||
@customElement("ak-application-wizard-commit-application")
|
@customElement("ak-application-wizard-commit-application")
|
||||||
export class ApplicationWizardCommitApplication extends BasePanel {
|
export class ApplicationWizardCommitApplication extends BasePanel {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
@ -134,10 +138,25 @@ export class ApplicationWizardCommitApplication extends BasePanel {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
.catch(async (resolution: any) => {
|
.catch(async (resolution: any) => {
|
||||||
const errors = await parseAPIError(resolution);
|
const errors = await parseAPIError(resolution);
|
||||||
|
|
||||||
|
// THIS is a really gross special case; if the user is duplicating the name of an
|
||||||
|
// existing provider, the error appears on the `app` (!) error object. We have to
|
||||||
|
// move that to the `provider.name` error field so it shows up in the right place.
|
||||||
|
if (isValidationError(errors) && Array.isArray(errors?.app?.provider)) {
|
||||||
|
const providerError = errors.app.provider;
|
||||||
|
errors.provider = errors.provider ?? {};
|
||||||
|
errors.provider.name = providerError;
|
||||||
|
delete errors.app.provider;
|
||||||
|
if (Object.keys(errors.app).length === 0) {
|
||||||
|
delete errors.app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errors = errors;
|
||||||
this.dispatchWizardUpdate({
|
this.dispatchWizardUpdate({
|
||||||
update: {
|
update: {
|
||||||
...this.wizard,
|
...this.wizard,
|
||||||
errors,
|
errors: this.errors,
|
||||||
},
|
},
|
||||||
status: "failed",
|
status: "failed",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,6 +11,10 @@ import {
|
|||||||
redirectUriHelp,
|
redirectUriHelp,
|
||||||
subjectModeOptions,
|
subjectModeOptions,
|
||||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
|
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
|
||||||
|
import {
|
||||||
|
IRedirectURIInput,
|
||||||
|
akOAuthRedirectURIInput,
|
||||||
|
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||||
import {
|
import {
|
||||||
makeSourceSelector,
|
makeSourceSelector,
|
||||||
oauth2SourcesProvider,
|
oauth2SourcesProvider,
|
||||||
@ -31,7 +35,13 @@ import { customElement, state } from "@lit/reactive-element/decorators.js";
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import { ClientTypeEnum, FlowsInstancesListDesignationEnum, SourcesApi } from "@goauthentik/api";
|
import {
|
||||||
|
ClientTypeEnum,
|
||||||
|
FlowsInstancesListDesignationEnum,
|
||||||
|
MatchingModeEnum,
|
||||||
|
RedirectURI,
|
||||||
|
SourcesApi,
|
||||||
|
} from "@goauthentik/api";
|
||||||
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
||||||
|
|
||||||
import BaseProviderPanel from "../BaseProviderPanel";
|
import BaseProviderPanel from "../BaseProviderPanel";
|
||||||
@ -120,14 +130,27 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
|
|||||||
>
|
>
|
||||||
</ak-text-input>
|
</ak-text-input>
|
||||||
|
|
||||||
<ak-textarea-input
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Redirect URIs/Origins")}
|
||||||
|
required
|
||||||
name="redirectUris"
|
name="redirectUris"
|
||||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
|
||||||
.value=${provider?.redirectUris}
|
|
||||||
.errorMessages=${errors?.redirectUriHelp ?? []}
|
|
||||||
.bighelp=${redirectUriHelp}
|
|
||||||
>
|
>
|
||||||
</ak-textarea-input>
|
<ak-array-input
|
||||||
|
.items=${[]}
|
||||||
|
.newItem=${() => ({
|
||||||
|
matchingMode: MatchingModeEnum.Strict,
|
||||||
|
url: "",
|
||||||
|
})}
|
||||||
|
.row=${(f?: RedirectURI) =>
|
||||||
|
akOAuthRedirectURIInput({
|
||||||
|
".redirectURI": f,
|
||||||
|
"style": "width: 100%",
|
||||||
|
"name": "oauth2-redirect-uri",
|
||||||
|
} as unknown as IRedirectURIInput)}
|
||||||
|
>
|
||||||
|
</ak-array-input>
|
||||||
|
${redirectUriHelp}
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Signing Key")}
|
label=${msg("Signing Key")}
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||||
|
import {
|
||||||
|
IRedirectURIInput,
|
||||||
|
akOAuthRedirectURIInput,
|
||||||
|
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-radio-input";
|
import "@goauthentik/components/ak-radio-input";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import "@goauthentik/components/ak-textarea-input";
|
import "@goauthentik/components/ak-textarea-input";
|
||||||
|
import "@goauthentik/elements/ak-array-input.js";
|
||||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
|
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
|
||||||
import "@goauthentik/elements/forms/FormGroup";
|
import "@goauthentik/elements/forms/FormGroup";
|
||||||
@ -15,7 +20,7 @@ import "@goauthentik/elements/forms/SearchSelect";
|
|||||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, css, html } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
@ -23,8 +28,10 @@ import {
|
|||||||
ClientTypeEnum,
|
ClientTypeEnum,
|
||||||
FlowsInstancesListDesignationEnum,
|
FlowsInstancesListDesignationEnum,
|
||||||
IssuerModeEnum,
|
IssuerModeEnum,
|
||||||
|
MatchingModeEnum,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
ProvidersApi,
|
ProvidersApi,
|
||||||
|
RedirectURI,
|
||||||
SubModeEnum,
|
SubModeEnum,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@ -98,13 +105,13 @@ export const issuerModeOptions = [
|
|||||||
|
|
||||||
const redirectUriHelpMessages = [
|
const redirectUriHelpMessages = [
|
||||||
msg(
|
msg(
|
||||||
"Valid redirect URLs after a successful authorization flow. Also specify any origins here for Implicit flows.",
|
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
|
||||||
),
|
),
|
||||||
msg(
|
msg(
|
||||||
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
|
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
|
||||||
),
|
),
|
||||||
msg(
|
msg(
|
||||||
'To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.',
|
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -124,11 +131,23 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
|||||||
@state()
|
@state()
|
||||||
showClientSecret = true;
|
showClientSecret = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
redirectUris: RedirectURI[] = [];
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return super.styles.concat(css`
|
||||||
|
ak-array-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
async loadInstance(pk: number): Promise<OAuth2Provider> {
|
async loadInstance(pk: number): Promise<OAuth2Provider> {
|
||||||
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
|
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
|
||||||
id: pk,
|
id: pk,
|
||||||
});
|
});
|
||||||
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
|
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
|
||||||
|
this.redirectUris = provider.redirectUris;
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,13 +222,24 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
|||||||
?hidden=${!this.showClientSecret}
|
?hidden=${!this.showClientSecret}
|
||||||
>
|
>
|
||||||
</ak-text-input>
|
</ak-text-input>
|
||||||
<ak-textarea-input
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Redirect URIs/Origins")}
|
||||||
|
required
|
||||||
name="redirectUris"
|
name="redirectUris"
|
||||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
|
||||||
.value=${provider?.redirectUris}
|
|
||||||
.bighelp=${redirectUriHelp}
|
|
||||||
>
|
>
|
||||||
</ak-textarea-input>
|
<ak-array-input
|
||||||
|
.items=${this.instance?.redirectUris ?? []}
|
||||||
|
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
|
||||||
|
.row=${(f?: RedirectURI) =>
|
||||||
|
akOAuthRedirectURIInput({
|
||||||
|
".redirectURI": f,
|
||||||
|
"style": "width: 100%",
|
||||||
|
"name": "oauth2-redirect-uri",
|
||||||
|
} as unknown as IRedirectURIInput)}
|
||||||
|
>
|
||||||
|
</ak-array-input>
|
||||||
|
${redirectUriHelp}
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||||
|
|||||||
104
web/src/admin/providers/oauth2/OAuth2ProviderRedirectURI.ts
Normal file
104
web/src/admin/providers/oauth2/OAuth2ProviderRedirectURI.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||||
|
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
|
||||||
|
import { type Spread } from "@goauthentik/elements/types";
|
||||||
|
import { spread } from "@open-wc/lit-helpers";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { css, html } from "lit";
|
||||||
|
import { customElement, property, queryAll } from "lit/decorators.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||||
|
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
|
import { MatchingModeEnum, RedirectURI } from "@goauthentik/api";
|
||||||
|
|
||||||
|
export interface IRedirectURIInput {
|
||||||
|
redirectURI: RedirectURI;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ak-provider-oauth2-redirect-uri")
|
||||||
|
export class OAuth2ProviderRedirectURI extends AkControlElement<RedirectURI> {
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFInputGroup,
|
||||||
|
PFFormControl,
|
||||||
|
css`
|
||||||
|
.pf-c-input-group select {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: Object, attribute: false })
|
||||||
|
redirectURI: RedirectURI = {
|
||||||
|
matchingMode: MatchingModeEnum.Strict,
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
@queryAll(".ak-form-control")
|
||||||
|
controls?: HTMLInputElement[];
|
||||||
|
|
||||||
|
json() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
|
||||||
|
) as unknown as RedirectURI;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isValid() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const onChange = () => {
|
||||||
|
this.dispatchEvent(new Event("change", { composed: true, bubbles: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`<div class="pf-c-input-group">
|
||||||
|
<select
|
||||||
|
name="matchingMode"
|
||||||
|
class="pf-c-form-control ak-form-control"
|
||||||
|
@change=${onChange}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="${MatchingModeEnum.Strict}"
|
||||||
|
?selected=${this.redirectURI.matchingMode === MatchingModeEnum.Strict}
|
||||||
|
>
|
||||||
|
${msg("Strict")}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="${MatchingModeEnum.Regex}"
|
||||||
|
?selected=${this.redirectURI.matchingMode === MatchingModeEnum.Regex}
|
||||||
|
>
|
||||||
|
${msg("Regex")}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
@change=${onChange}
|
||||||
|
value="${ifDefined(this.redirectURI.url ?? undefined)}"
|
||||||
|
class="pf-c-form-control ak-form-control"
|
||||||
|
required
|
||||||
|
id="url"
|
||||||
|
placeholder=${msg("URL")}
|
||||||
|
name="url"
|
||||||
|
tabindex="1"
|
||||||
|
/>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function akOAuthRedirectURIInput(properties: IRedirectURIInput) {
|
||||||
|
return html`<ak-provider-oauth2-redirect-uri
|
||||||
|
${spread(properties as unknown as Spread)}
|
||||||
|
></ak-provider-oauth2-redirect-uri>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-provider-oauth2-redirect-uri": OAuth2ProviderRedirectURI;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -234,7 +234,11 @@ export class OAuth2ProviderViewPage extends AKElement {
|
|||||||
</dt>
|
</dt>
|
||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">
|
<div class="pf-c-description-list__text">
|
||||||
${this.provider.redirectUris}
|
<ul>
|
||||||
|
${this.provider.redirectUris.map((ru) => {
|
||||||
|
return html`<li>${ru.matchingMode}: ${ru.url}</li>`;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -392,9 +392,13 @@ export class ProxyProviderViewPage extends AKElement {
|
|||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">
|
<div class="pf-c-description-list__text">
|
||||||
<ul class="pf-c-list">
|
<ul class="pf-c-list">
|
||||||
${this.provider.redirectUris.split("\n").map((url) => {
|
<ul>
|
||||||
return html`<li><pre>${url}</pre></li>`;
|
${this.provider.redirectUris.map((ru) => {
|
||||||
})}
|
return html`<li>
|
||||||
|
${ru.matchingMode}: ${ru.url}
|
||||||
|
</li>`;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { convertToSlug } from "@goauthentik/common/utils";
|
|||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { FormGroup } from "@goauthentik/elements/forms/FormGroup";
|
import { FormGroup } from "@goauthentik/elements/forms/FormGroup";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { CSSResult, css } from "lit";
|
import { CSSResult, css } from "lit";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
@ -86,7 +86,7 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
writeOnlyActivated = false;
|
writeOnlyActivated = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
errorMessages: string[] = [];
|
errorMessages: string[] | string[][] = [];
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
slugMode = false;
|
slugMode = false;
|
||||||
@ -183,6 +183,16 @@ export class HorizontalFormElement extends AKElement {
|
|||||||
</p>`
|
</p>`
|
||||||
: html``}
|
: html``}
|
||||||
${this.errorMessages.map((message) => {
|
${this.errorMessages.map((message) => {
|
||||||
|
if (message instanceof Object) {
|
||||||
|
return html`${Object.entries(message).map(([field, errMsg]) => {
|
||||||
|
return html`<p
|
||||||
|
class="pf-c-form__helper-text pf-m-error"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
${msg(str`${field}: ${errMsg}`)}
|
||||||
|
</p>`;
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
return html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
return html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||||
${message}
|
${message}
|
||||||
</p>`;
|
</p>`;
|
||||||
|
|||||||
@ -5741,9 +5741,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="s070fdfb03034ca9b">
|
<trans-unit id="s070fdfb03034ca9b">
|
||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -7053,6 +7050,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -6006,9 +6006,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="s070fdfb03034ca9b">
|
<trans-unit id="s070fdfb03034ca9b">
|
||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -7318,6 +7315,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -5658,9 +5658,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="s070fdfb03034ca9b">
|
<trans-unit id="s070fdfb03034ca9b">
|
||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -6970,6 +6967,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -7542,10 +7542,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>Un indice, l'assistant nouvelle application est actuellement caché</target>
|
<target>Un indice, l'assistant nouvelle application est actuellement caché</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>Applications externes qui utilisent authentik comme fournisseur d'identité, en utilisant des protocoles comme OAuth2 et SAML. Toutes les applications sont affichées ici, même celles auxquelles vous n'avez pas accès.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>Message de refus</target>
|
<target>Message de refus</target>
|
||||||
@ -9278,6 +9274,9 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -7498,10 +7498,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>Un suggerimento, "New Application Wizard", è attualmente nascosto</target>
|
<target>Un suggerimento, "New Application Wizard", è attualmente nascosto</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>Applicazioni esterne che utilizzano Authenk come fornitore di identità tramite protocolli come OAuth2 e SAML. Tutte le applicazioni sono mostrate qui, anche quelle a cui non è possibile accedere.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>Negare il messaggio</target>
|
<target>Negare il messaggio</target>
|
||||||
@ -9251,6 +9247,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sc24af6de78468cfa">
|
<trans-unit id="sc24af6de78468cfa">
|
||||||
<source>Require administrators to provide a reason for impersonating a user.</source>
|
<source>Require administrators to provide a reason for impersonating a user.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -7512,10 +7512,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>힌트, '새 애플리케이션 마법사'는 현재, 숨겨져 있습니다.</target>
|
<target>힌트, '새 애플리케이션 마법사'는 현재, 숨겨져 있습니다.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>OAuth2 및 SAML과 같은 프로토콜을 통해 인증서를 ID 공급자로 사용하는 외부 애플리케이션. 액세스할 수 없는 애플리케이션을 포함한 모든 애플리케이션이 여기에 표시됩니다.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>거부 메시지</target>
|
<target>거부 메시지</target>
|
||||||
@ -8885,6 +8881,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -7495,9 +7495,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
|||||||
<trans-unit id="s2b1c81130a65a55b">
|
<trans-unit id="s2b1c81130a65a55b">
|
||||||
<source>Sync currently running.</source>
|
<source>Sync currently running.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="sb35c08e3a541188f">
|
<trans-unit id="sb35c08e3a541188f">
|
||||||
<source>Also known as Client ID.</source>
|
<source>Also known as Client ID.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -8732,6 +8729,9 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -7546,10 +7546,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>Jedna podpowiedź, „Kreator nowej aplikacji”, jest obecnie ukryty</target>
|
<target>Jedna podpowiedź, „Kreator nowej aplikacji”, jest obecnie ukryty</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>Aplikacje zewnętrzne, które używają authentik jako dostawcy tożsamości za pośrednictwem protokołów takich jak OAuth2 i SAML. Tutaj wyświetlane są wszystkie aplikacje, nawet te, do których nie masz dostępu.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>Komunikat odmowy</target>
|
<target>Komunikat odmowy</target>
|
||||||
@ -9148,6 +9144,9 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -7490,10 +7490,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>Ōńē ĥĩńţ, 'Ńēŵ Àƥƥĺĩćàţĩōń Ŵĩźàŕď', ĩś ćũŕŕēńţĺŷ ĥĩďďēń</target>
|
<target>Ōńē ĥĩńţ, 'Ńēŵ Àƥƥĺĩćàţĩōń Ŵĩźàŕď', ĩś ćũŕŕēńţĺŷ ĥĩďďēń</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>Ēxţēŕńàĺ àƥƥĺĩćàţĩōńś ţĥàţ ũśē àũţĥēńţĩķ àś àń ĩďēńţĩţŷ ƥŕōvĩďēŕ vĩà ƥŕōţōćōĺś ĺĩķē ŌÀũţĥ2 àńď ŚÀḾĹ. Àĺĺ àƥƥĺĩćàţĩōńś àŕē śĥōŵń ĥēŕē, ēvēń ōńēś ŷōũ ćàńńōţ àććēśś.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>Ďēńŷ ḿēśśàĝē</target>
|
<target>Ďēńŷ ḿēśśàĝē</target>
|
||||||
@ -9188,4 +9184,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
|
</trans-unit>
|
||||||
</body></file></xliff>
|
</body></file></xliff>
|
||||||
|
|||||||
@ -7545,10 +7545,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>Одна подсказка, "Мастер создания нового приложения", в настоящее время скрыта</target>
|
<target>Одна подсказка, "Мастер создания нового приложения", в настоящее время скрыта</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>Внешние приложения, использующие authentik в качестве поставщика идентификационных данных по таким протоколам, как OAuth2 и SAML. Здесь показаны все приложения, даже те, к которым вы не можете получить доступ.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>Запретить сообщение</target>
|
<target>Запретить сообщение</target>
|
||||||
@ -9211,6 +9207,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -7495,10 +7495,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>Bir ipucu, 'Yeni Uygulama Sihirbazı' şu anda gizli</target>
|
<target>Bir ipucu, 'Yeni Uygulama Sihirbazı' şu anda gizli</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>OAuth2 ve SAML gibi protokoller aracılığıyla kimlik sağlayıcı olarak authentik kullanan harici uygulamalar. Erişemedikleriniz de dahil olmak üzere tüm uygulamalar burada gösterilir.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>İletiyi reddet</target>
|
<target>İletiyi reddet</target>
|
||||||
@ -9241,6 +9237,9 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -1989,9 +1989,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||||||
<trans-unit id="s6ba50bb0842ba1e2">
|
<trans-unit id="s6ba50bb0842ba1e2">
|
||||||
<source>Applications</source>
|
<source>Applications</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s96b2fefc550e4b1c">
|
<trans-unit id="s96b2fefc550e4b1c">
|
||||||
<source>Provider Type</source>
|
<source>Provider Type</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -5902,6 +5899,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||||
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="s4caed5b7a7e5d89b">
|
<trans-unit id="s4caed5b7a7e5d89b">
|
||||||
@ -596,9 +596,9 @@
|
|||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="saa0e2675da69651b">
|
<trans-unit id="saa0e2675da69651b">
|
||||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||||
<target>未找到 URL "
|
<target>未找到 URL "
|
||||||
<x id="0" equiv-text="${this.url}"/>"。</target>
|
<x id="0" equiv-text="${this.url}"/>"。</target>
|
||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s58cd9c2fe836d9c6">
|
<trans-unit id="s58cd9c2fe836d9c6">
|
||||||
@ -1030,8 +1030,8 @@
|
|||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sa8384c9c26731f83">
|
<trans-unit id="sa8384c9c26731f83">
|
||||||
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
|
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
|
||||||
<target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
|
<target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
|
||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s55787f4dfcdce52b">
|
<trans-unit id="s55787f4dfcdce52b">
|
||||||
@ -1752,8 +1752,8 @@
|
|||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sa90b7809586c35ce">
|
<trans-unit id="sa90b7809586c35ce">
|
||||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||||
<target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
|
<target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
|
||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s0410779cb47de312">
|
<trans-unit id="s0410779cb47de312">
|
||||||
@ -2916,8 +2916,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s76768bebabb7d543">
|
<trans-unit id="s76768bebabb7d543">
|
||||||
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
|
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
|
||||||
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
|
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
|
||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s026555347e589f0e">
|
<trans-unit id="s026555347e589f0e">
|
||||||
@ -3663,8 +3663,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s7b1fba26d245cb1c">
|
<trans-unit id="s7b1fba26d245cb1c">
|
||||||
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
|
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
|
||||||
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
|
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
|
||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s44536d20bb5c8257">
|
<trans-unit id="s44536d20bb5c8257">
|
||||||
@ -3840,10 +3840,10 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sa95a538bfbb86111">
|
<trans-unit id="sa95a538bfbb86111">
|
||||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||||
<target>您确定要更新
|
<target>您确定要更新
|
||||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||||
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
|
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
|
||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sc92d7cfb6ee1fec6">
|
<trans-unit id="sc92d7cfb6ee1fec6">
|
||||||
@ -4919,7 +4919,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sdf1d8edef27236f0">
|
<trans-unit id="sdf1d8edef27236f0">
|
||||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||||
<target>像 YubiKey 这样的“漫游”身份验证器</target>
|
<target>像 YubiKey 这样的“漫游”身份验证器</target>
|
||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -5298,7 +5298,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s1608b2f94fa0dbd4">
|
<trans-unit id="s1608b2f94fa0dbd4">
|
||||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||||
<target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target>
|
<target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target>
|
||||||
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -7544,10 +7544,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>“新应用程序向导”提示目前已隐藏</target>
|
<target>“新应用程序向导”提示目前已隐藏</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>通过 OAuth2 和 SAML 等协议,使用 authentik 作为身份提供程序的外部应用程序。此处显示了所有应用程序,即使您无法访问的也包括在内。</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>拒绝消息</target>
|
<target>拒绝消息</target>
|
||||||
@ -7713,7 +7709,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
|
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s824e0943a7104668">
|
<trans-unit id="s824e0943a7104668">
|
||||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||||
<target>此用户将会被添加到组 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;。</target>
|
<target>此用户将会被添加到组 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;。</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s62e7f6ed7d9cb3ca">
|
<trans-unit id="s62e7f6ed7d9cb3ca">
|
||||||
@ -9063,7 +9059,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<target>同步组</target>
|
<target>同步组</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s2d5f69929bb7221d">
|
<trans-unit id="s2d5f69929bb7221d">
|
||||||
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
|
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
|
||||||
<target><x id="0" equiv-text="${prompt.name}"/>(&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;,类型为 <x id="2" equiv-text="${prompt.type}"/>)</target>
|
<target><x id="0" equiv-text="${prompt.name}"/>(&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;,类型为 <x id="2" equiv-text="${prompt.type}"/>)</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sa38c5a2731be3a46">
|
<trans-unit id="sa38c5a2731be3a46">
|
||||||
@ -9297,6 +9293,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
<target>此选项配置流程执行器页面上的页脚链接。URL 限为 Web 和电子邮件地址。如果名称留空,则显示 URL 自身。</target>
|
<target>此选项配置流程执行器页面上的页脚链接。URL 限为 Web 和电子邮件地址。如果名称留空,则显示 URL 自身。</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -5699,9 +5699,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="s070fdfb03034ca9b">
|
<trans-unit id="s070fdfb03034ca9b">
|
||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -7011,6 +7008,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -7486,10 +7486,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||||
<target>提示:「新增應用程式設定精靈」目前處於隱藏中</target>
|
<target>提示:「新增應用程式設定精靈」目前處於隱藏中</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s61bd841e66966325">
|
|
||||||
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
|
||||||
<target>使用 authentik 作為身份供應商的外部應用程式,透過像 OAuth2 和 SAML 這樣的協議。此處顯示所有應用程式,即使是您無法存取的應用程式也包括在內。</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="s1cc306d8e28c4464">
|
<trans-unit id="s1cc306d8e28c4464">
|
||||||
<source>Deny message</source>
|
<source>Deny message</source>
|
||||||
<target>拒絕的訊息</target>
|
<target>拒絕的訊息</target>
|
||||||
@ -8846,6 +8842,9 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s92205c10ba1f0f4c">
|
<trans-unit id="s92205c10ba1f0f4c">
|
||||||
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s66f572bec2bde9c4">
|
||||||
|
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || "authentik"}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|||||||
@ -78,7 +78,7 @@ Short summary of the issue
|
|||||||
|
|
||||||
### Patches
|
### Patches
|
||||||
|
|
||||||
authentik x, y and z fix this issue, for other versions the workaround can be used.
|
authentik x, y and z fix this issue, for other versions the workaround below can be used.
|
||||||
|
|
||||||
### Impact
|
### Impact
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ Describe a workaround if possible
|
|||||||
|
|
||||||
If you have any questions or comments about this advisory:
|
If you have any questions or comments about this advisory:
|
||||||
|
|
||||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@ -157,6 +157,30 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.10
|
|||||||
- stages/password: use recovery flow from brand (cherry-pick #11953) (#11969)
|
- stages/password: use recovery flow from brand (cherry-pick #11953) (#11969)
|
||||||
- web: bump API Client version (#11992)
|
- web: bump API Client version (#11992)
|
||||||
|
|
||||||
|
## Fixed in 2024.10.3
|
||||||
|
|
||||||
|
- core: fix source_flow_manager throwing error when authenticated user attempts to re-authenticate with existing link (cherry-pick #12080) (#12081)
|
||||||
|
- internal: add CSP header to files in `/media` (cherry-pick #12092) (#12108)
|
||||||
|
- providers/ldap: fix global search_full_directory permission not being sufficient (cherry-pick #12028) (#12030)
|
||||||
|
- providers/scim: accept string and int for SCIM IDs (cherry-pick #12093) (#12095)
|
||||||
|
- rbac: fix incorrect object_description for object-level permissions (cherry-pick #12029) (#12043)
|
||||||
|
- root: check remote IP for proxy protocol same as HTTP/etc (cherry-pick #12094) (#12097)
|
||||||
|
- root: fix activation of locale not being scoped (cherry-pick #12091) (#12096)
|
||||||
|
- security: fix [CVE-2024-52287](../../security/cves/CVE-2024-52287.md), reported by [@matt1097](https://github.com/matt1097) (#12117)
|
||||||
|
- security: fix [CVE-2024-52289](../../security/cves/CVE-2024-52289.md), reported by [@PontusHanssen](https://github.com/PontusHanssen) (#12113)
|
||||||
|
- security: fix [CVE-2024-52307](../../security/cves/CVE-2024-52307.md), reported by [@mgerstner](https://github.com/mgerstner) (#12115)
|
||||||
|
- web/admin: better footer links (#12004)
|
||||||
|
- web/flows: fix invisible captcha call (cherry-pick #12048) (#12049)
|
||||||
|
- website/docs: add CSP to hardening (cherry-pick #11970) (#12116)
|
||||||
|
|
||||||
|
## Fixed in 2024.10.4
|
||||||
|
|
||||||
|
- providers/oauth2: fix migration (cherry-pick #12138) (#12139)
|
||||||
|
- providers/oauth2: fix migration dependencies (cherry-pick #12123) (#12132)
|
||||||
|
- providers/oauth2: fix redirect uri input (cherry-pick #12122) (#12127)
|
||||||
|
- providers/proxy: fix redirect_uri (cherry-pick #12121) (#12125)
|
||||||
|
- web: bump API Client version (cherry-pick #12129) (#12130)
|
||||||
|
|
||||||
## API Changes
|
## API Changes
|
||||||
|
|
||||||
### API Changes in 2024.10.0
|
### API Changes in 2024.10.0
|
||||||
|
|||||||
@ -300,6 +300,21 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.8
|
|||||||
- web/admin: fix invalid create date shown for MFA registered before date was saved (cherry-pick #11728) (#11729)
|
- web/admin: fix invalid create date shown for MFA registered before date was saved (cherry-pick #11728) (#11729)
|
||||||
- web/admin: fix sync single button throwing error (cherry-pick #11727) (#11730)
|
- web/admin: fix sync single button throwing error (cherry-pick #11727) (#11730)
|
||||||
|
|
||||||
|
## Fixed in 2024.8.5
|
||||||
|
|
||||||
|
- security: fix [CVE-2024-52287](../../security/cves/CVE-2024-52287.md), reported by [@matt1097](https://github.com/matt1097) (#12114)
|
||||||
|
- security: fix [CVE-2024-52289](../../security/cves/CVE-2024-52289.md), reported by [@PontusHanssen](https://github.com/PontusHanssen) (#12113)
|
||||||
|
- security: fix [CVE-2024-52307](../../security/cves/CVE-2024-52307.md), reported by [@mgerstner](https://github.com/mgerstner) (#12115)
|
||||||
|
- web/admin: better footer links (#12004)
|
||||||
|
- web: bump API Client version (#12118)
|
||||||
|
|
||||||
|
## Fixed in 2024.8.6
|
||||||
|
|
||||||
|
- providers/oauth2: fix migration (cherry-pick #12138) (#12140)
|
||||||
|
- providers/oauth2: fix redirect uri input (cherry-pick #12122) (#12128)
|
||||||
|
- providers/proxy: fix redirect_uri (cherry-pick #12121) (#12126)
|
||||||
|
- web: bump API Client version (cherry-pick #12129) (#12131)
|
||||||
|
|
||||||
## API Changes
|
## API Changes
|
||||||
|
|
||||||
#### What's New
|
#### What's New
|
||||||
|
|||||||
27
website/docs/security/cves/CVE-2024-52287.md
Normal file
27
website/docs/security/cves/CVE-2024-52287.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# CVE-2024-52287
|
||||||
|
|
||||||
|
_Reported by [@matt1097](https://github.com/matt1097)_
|
||||||
|
|
||||||
|
## Insufficient validation of OAuth scopes for client_credentials and device_code grants
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
When using the `client_credentials` or `device_code` OAuth grants, it was possible for an attacker to get a token from authentik with scopes that haven't been configured in authentik.
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
With the `device_code` grant, it was possible to have a user authorize a set of permitted scopes, and then acquire a token with a different set of scopes, including scopes not configured. This token could potentially be used to send requests to another system which trusts tokens signed by authentik and execute malicious actions on behalf of the user.
|
||||||
|
|
||||||
|
With the `client_credentials` grant, because there is no user authorization process, authentik would not validate the scopes requested for the token, allowing tokens to be issued with scopes not configured in authentik. These could similarly be used to execute malicious actions in other systems.
|
||||||
|
|
||||||
|
There is no workaround for this issue; however this issue could only be exploited if an attacker possesses a valid set of OAuth2 `client_id` and `client_secret` credentials, and has the knowledge of another system that trusts tokens issued by authentik and what scopes it checks for.
|
||||||
|
|
||||||
|
### Patches
|
||||||
|
|
||||||
|
authentik 2024.8.5 and 2024.10.3 fix this issue.
|
||||||
|
|
||||||
|
### For more information
|
||||||
|
|
||||||
|
If you have any questions or comments about this advisory:
|
||||||
|
|
||||||
|
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||||
30
website/docs/security/cves/CVE-2024-52289.md
Normal file
30
website/docs/security/cves/CVE-2024-52289.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# CVE-2024-52289
|
||||||
|
|
||||||
|
_Reported by [@PontusHanssen](https://github.com/PontusHanssen)_
|
||||||
|
|
||||||
|
## Insecure default configuration for OAuth2 Redirect URIs
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Redirect URIs in the OAuth2 provider in authentik are checked by RegEx comparison.
|
||||||
|
When no Redirect URIs are configured in a provider, authentik will automatically use the first `redirect_uri` value received as an allowed redirect URI, without escaping characters that have a special meaning in RegEx. Similarly, the documentation did not take this into consideration either.
|
||||||
|
|
||||||
|
Given a provider with the Redirect URIs set to `https://foo.example.com`, an attacker can register a domain `fooaexample.com`, and it will correctly pass validation.
|
||||||
|
|
||||||
|
### Patches
|
||||||
|
|
||||||
|
authentik 2024.8.5 and 2024.10.3 fix this issue.
|
||||||
|
|
||||||
|
The patched versions remedy this issue by changing the format that the Redirect URIs are saved in, allowing for the explicit configuration if the URL should be checked strictly or as a RegEx. This means that these patches include a backwards-incompatible database change and API change.
|
||||||
|
|
||||||
|
Manual action _is required_ if any provider is intended to use RegEx for Redirect URIs because the migration will set the comparison type to strict for every Redirect URI.
|
||||||
|
|
||||||
|
### Workarounds
|
||||||
|
|
||||||
|
When configuring OAuth2 providers, make sure to escape any wildcard characters that are not intended to function as a wildcard, for example replace `.` with `\.`.
|
||||||
|
|
||||||
|
### For more information
|
||||||
|
|
||||||
|
If you have any questions or comments about this advisory:
|
||||||
|
|
||||||
|
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||||
36
website/docs/security/cves/CVE-2024-52307.md
Normal file
36
website/docs/security/cves/CVE-2024-52307.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# CVE-2024-52307
|
||||||
|
|
||||||
|
_Reported by [@mgerstner](https://github.com/mgerstner)_
|
||||||
|
|
||||||
|
## Timing attack due to a lack of constant time comparison for metrics view
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Due to the usage of a non-constant time comparison for the `/-/metrics/` endpoint it was possible to brute-force the `SECRET_KEY`, which is used to authenticate the endpoint. The `/-/metrics/` endpoint returns Prometheus metrics and is not intended to be accessed directly, as the Go proxy running in the authentik server container fetches data from this endpoint and serves it on a separate port (9300 by default), which can be scraped by Prometheus without being exposed publicly.
|
||||||
|
|
||||||
|
### Patches
|
||||||
|
|
||||||
|
authentik 2024.8.5 and 2024.10.3 fix this issue, for other versions the workaround below can be used.
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
|
||||||
|
With enough attempts the `SECRET_KEY` of the authentik installation can be brute-forced, which can be used to sign new or modify existing cookies.
|
||||||
|
|
||||||
|
### Workarounds
|
||||||
|
|
||||||
|
Since the `/-/metrics/` endpoint is not intended to be accessed publicly, requests to the endpoint can be blocked by the reverse proxy/load balancer used in conjunction with authentik.
|
||||||
|
|
||||||
|
For example for nginx:
|
||||||
|
|
||||||
|
```
|
||||||
|
location /-/metrics/ {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### For more information
|
||||||
|
|
||||||
|
If you have any questions or comments about this advisory:
|
||||||
|
|
||||||
|
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
|
||||||
@ -47,3 +47,32 @@ To prevent any user from creating/editing CAPTCHA stages block API requests to t
|
|||||||
- `/api/v3/managed/blueprints*`
|
- `/api/v3/managed/blueprints*`
|
||||||
|
|
||||||
With these restrictions in place, CAPTCHA stages can only be edited using [Blueprints on the file system](../customize/blueprints/index.md#storage---file).
|
With these restrictions in place, CAPTCHA stages can only be edited using [Blueprints on the file system](../customize/blueprints/index.md#storage---file).
|
||||||
|
|
||||||
|
### Content Security Policy (CSP)
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
Setting up CSP incorrectly might result in the client not loading necessary third-party code.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
In some cases, a CSP header will already be set by authentik (for example, in [user uploaded content](https://github.com/goauthentik/authentik/pull/12092/)). Do not overwrite an already existing header as doing so might result in vulnerabilities. Instead, add a new CSP header.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Content Security Policy (CSP) is a security standard that mitigates the risk of content injection vulnerabilities. authentik doesn't currently support CSP natively, so setting it up depends on your installation. We recommend using a [reverse proxy](../install-config/reverse-proxy.md) to set a CSP header.
|
||||||
|
|
||||||
|
authentik requires at least the following allowed locations:
|
||||||
|
|
||||||
|
```
|
||||||
|
default-src 'self';
|
||||||
|
img-src 'https:' 'http:' 'data:';
|
||||||
|
object-src 'none';
|
||||||
|
style-src 'self' 'unsafe-inline'; # Required due to Lit/ShadowDOM
|
||||||
|
script-src 'self' 'unsafe-inline'; # Required for generated scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
Your use case might require more allowed locations for various directives, e.g.
|
||||||
|
|
||||||
|
- when using a CAPTCHA service
|
||||||
|
- when using Sentry
|
||||||
|
- when using any custom JavaScript in a prompt stage
|
||||||
|
- when using Spotlight Sidecar for development
|
||||||
|
|||||||
@ -35,7 +35,7 @@ Enable the ability for users to change their Email address, defaults to `false`.
|
|||||||
|
|
||||||
### Allow users to change username
|
### Allow users to change username
|
||||||
|
|
||||||
Enable the ability for users to change their Usernames, defaults to `false`.
|
Enable the ability for users to change their usernames, defaults to `false`.
|
||||||
|
|
||||||
### Event retention
|
### Event retention
|
||||||
|
|
||||||
@ -43,15 +43,11 @@ Configure how long [Events](./events/index.md) are retained for within authentik
|
|||||||
|
|
||||||
### Footer links
|
### Footer links
|
||||||
|
|
||||||
This option configures the footer links on the flow executor pages.
|
This option allows you to add linked text (footer links) on the bottom of flow pages. You can also use this setting to display additional static text to the flow pages, even if no URL is provided.
|
||||||
|
|
||||||
The setting can be used as follows:
|
The URL is limited to web and email addresses. If the name is left blank, the URL will be shown.
|
||||||
|
|
||||||
```json
|
This is a global setting. All flow pages that are rendered by the [Flow Executor](../add-secure-apps/flows-stages/flow/executors/if-flow.md) will display the footer links.
|
||||||
[{ "name": "Link Name", "href": "https://goauthentik.io" }]
|
|
||||||
```
|
|
||||||
|
|
||||||
Starting with authentik 2024.6.1, the `href` attribute is optional, and this option can be used to add additional text to the flow executor pages.
|
|
||||||
|
|
||||||
### GDPR compliance
|
### GDPR compliance
|
||||||
|
|
||||||
|
|||||||
@ -654,20 +654,41 @@ export default {
|
|||||||
type: "category",
|
type: "category",
|
||||||
label: "CVEs",
|
label: "CVEs",
|
||||||
items: [
|
items: [
|
||||||
"security/cves/CVE-2024-47077",
|
{
|
||||||
"security/cves/CVE-2024-47070",
|
type: "category",
|
||||||
"security/cves/CVE-2024-38371",
|
label: "2024",
|
||||||
"security/cves/CVE-2024-37905",
|
items: [
|
||||||
"security/cves/CVE-2024-23647",
|
"security/cves/CVE-2024-52307",
|
||||||
"security/cves/CVE-2024-21637",
|
"security/cves/CVE-2024-52289",
|
||||||
"security/cves/CVE-2023-48228",
|
"security/cves/CVE-2024-52287",
|
||||||
"security/cves/GHSA-rjvp-29xq-f62w",
|
"security/cves/CVE-2024-47077",
|
||||||
"security/cves/CVE-2023-39522",
|
"security/cves/CVE-2024-47070",
|
||||||
"security/cves/CVE-2023-36456",
|
"security/cves/CVE-2024-38371",
|
||||||
"security/cves/CVE-2023-26481",
|
"security/cves/CVE-2024-37905",
|
||||||
"security/cves/CVE-2022-23555",
|
"security/cves/CVE-2024-23647",
|
||||||
"security/cves/CVE-2022-46145",
|
"security/cves/CVE-2024-21637",
|
||||||
"security/cves/CVE-2022-46172",
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "2023",
|
||||||
|
items: [
|
||||||
|
"security/cves/CVE-2023-48228",
|
||||||
|
"security/cves/GHSA-rjvp-29xq-f62w",
|
||||||
|
"security/cves/CVE-2023-39522",
|
||||||
|
"security/cves/CVE-2023-36456",
|
||||||
|
"security/cves/CVE-2023-26481",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "2022",
|
||||||
|
items: [
|
||||||
|
"security/cves/CVE-2022-23555",
|
||||||
|
"security/cves/CVE-2022-46145",
|
||||||
|
"security/cves/CVE-2022-46172",
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user