Compare commits
54 Commits
web/cleanu
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
527e584699 | |||
80dfe371e6 | |||
a3d1491aee | |||
1b98792637 | |||
111e120220 | |||
20642d49c3 | |||
a9776a83d3 | |||
b9faae83b4 | |||
afc2998697 | |||
fabacc56c4 | |||
11b013d3b8 | |||
e10c47d8b8 | |||
d2b194f6b7 | |||
780a59c908 | |||
f8015fccd8 | |||
05f4e738a1 | |||
f535a23c03 | |||
91905530c7 | |||
40a970e321 | |||
b51d8d0ba3 | |||
7e8891338f | |||
3ae0001bb5 | |||
66a4970014 | |||
7ab9300761 | |||
a2eccd5022 | |||
31aeaa247f | |||
f49008bbb6 | |||
feb13c8ee5 | |||
d5ef831718 | |||
64676819ec | |||
7ed268fef4 | |||
f6526d1be9 | |||
12f8b4566b | |||
665de8ef22 | |||
9eaa723bf8 | |||
b2ca9c8cbc | |||
7927392100 | |||
d8d07e32cb | |||
f7c5d329eb | |||
92dec32547 | |||
510feccd31 | |||
364a9a1f02 | |||
40cbb7567b | |||
8ad0f63994 | |||
6ce33ab912 | |||
d96b577abd | |||
8c547589f6 | |||
3775e5b84f | |||
fa30339f65 | |||
e825eda106 | |||
246cae3dfa | |||
6cfd2bd1af | |||
f0e4f93fe6 | |||
434aa57ba7 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2024.10.0
|
current_version = 2024.10.4
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2024.10.0"
|
__version__ = "2024.10.4"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ API Browser - {{ brand.branding_title }}
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% versioned_script "dist/standalone/api-browser/index-%v.js" %}
|
<script src="{% versioned_script 'dist/standalone/api-browser/index-%v.js' %}" type="module"></script>
|
||||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
|
||||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -27,7 +27,8 @@ def blueprint_tester(file_name: Path) -> Callable:
|
|||||||
base = Path("blueprints/")
|
base = Path("blueprints/")
|
||||||
rel_path = Path(file_name).relative_to(base)
|
rel_path = Path(file_name).relative_to(base)
|
||||||
importer = Importer.from_string(BlueprintInstance(path=str(rel_path)).retrieve())
|
importer = Importer.from_string(BlueprintInstance(path=str(rel_path)).retrieve())
|
||||||
self.assertTrue(importer.validate()[0])
|
validation, logs = importer.validate()
|
||||||
|
self.assertTrue(validation, logs)
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
|
|
||||||
return tester
|
return tester
|
||||||
|
@ -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
|
||||||
|
with override(locale_to_set):
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Authenticator Devices API Views"""
|
"""Authenticator Devices API Views"""
|
||||||
|
|
||||||
|
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, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||||
from rest_framework.fields import (
|
from rest_framework.fields import (
|
||||||
@ -40,7 +41,11 @@ class DeviceSerializer(MetaNameSerializer):
|
|||||||
def get_extra_description(self, instance: Device) -> str:
|
def get_extra_description(self, instance: Device) -> str:
|
||||||
"""Get extra description"""
|
"""Get extra description"""
|
||||||
if isinstance(instance, WebAuthnDevice):
|
if isinstance(instance, WebAuthnDevice):
|
||||||
return instance.device_type.description
|
return (
|
||||||
|
instance.device_type.description
|
||||||
|
if instance.device_type
|
||||||
|
else _("Extra description not available")
|
||||||
|
)
|
||||||
if isinstance(instance, EndpointDevice):
|
if isinstance(instance, EndpointDevice):
|
||||||
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
||||||
return ""
|
return ""
|
||||||
|
@ -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,16 +31,18 @@ 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
|
||||||
|
|
||||||
|
with override(locale_to_set):
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -129,6 +129,11 @@ class SourceFlowManager:
|
|||||||
)
|
)
|
||||||
new_connection.user = self.request.user
|
new_connection.user = self.request.user
|
||||||
new_connection = self.update_user_connection(new_connection, **kwargs)
|
new_connection = self.update_user_connection(new_connection, **kwargs)
|
||||||
|
if existing := self.user_connection_type.objects.filter(
|
||||||
|
source=self.source, identifier=self.identifier
|
||||||
|
).first():
|
||||||
|
existing = self.update_user_connection(existing)
|
||||||
|
return Action.AUTH, existing
|
||||||
return Action.LINK, new_connection
|
return Action.LINK, new_connection
|
||||||
|
|
||||||
action, connection = self.matcher.get_user_action(self.identifier, self.user_properties)
|
action, connection = self.matcher.get_user_action(self.identifier, self.user_properties)
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
||||||
{% versioned_script "dist/poly-%v.js" %}
|
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||||
{% versioned_script "dist/standalone/loading/index-%v.js" %}
|
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% load authentik_core %}
|
{% load authentik_core %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% versioned_script "dist/admin/AdminInterface-%v.js" %}
|
<script src="{% versioned_script 'dist/admin/AdminInterface-%v.js' %}" type="module"></script>
|
||||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||||
{% include "base/header_js.html" %}
|
{% include "base/header_js.html" %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% load authentik_core %}
|
{% load authentik_core %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% versioned_script "dist/user/UserInterface-%v.js" %}
|
<script src="{% versioned_script 'dist/user/UserInterface-%v.js' %}" type="module"></script>
|
||||||
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
|
||||||
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
|
||||||
{% include "base/header_js.html" %}
|
{% include "base/header_js.html" %}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.templatetags.static import static as static_loader
|
from django.templatetags.static import static as static_loader
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
from authentik import get_full_version
|
from authentik import get_full_version
|
||||||
|
|
||||||
@ -12,10 +11,4 @@ register = template.Library()
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def versioned_script(path: str) -> str:
|
def versioned_script(path: str) -> str:
|
||||||
"""Wrapper around {% static %} tag that supports setting the version"""
|
"""Wrapper around {% static %} tag that supports setting the version"""
|
||||||
returned_lines = [
|
return static_loader(path.replace("%v", get_full_version()))
|
||||||
(
|
|
||||||
f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
|
|
||||||
'" type="module"></script>'
|
|
||||||
),
|
|
||||||
]
|
|
||||||
return mark_safe("".join(returned_lines)) # nosec
|
|
||||||
|
@ -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(
|
||||||
|
@ -81,6 +81,22 @@ class TestSourceFlowManager(TestCase):
|
|||||||
reverse("authentik_core:if-user") + "#/settings;page-sources",
|
reverse("authentik_core:if-user") + "#/settings;page-sources",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_authenticated_auth(self):
|
||||||
|
"""Test authenticated user linking"""
|
||||||
|
user = User.objects.create(username="foo", email="foo@bar.baz")
|
||||||
|
UserOAuthSourceConnection.objects.create(
|
||||||
|
user=user, source=self.source, identifier=self.identifier
|
||||||
|
)
|
||||||
|
request = get_request("/", user=user)
|
||||||
|
flow_manager = OAuthSourceFlowManager(
|
||||||
|
self.source, request, self.identifier, {"info": {}}, {}
|
||||||
|
)
|
||||||
|
action, connection = flow_manager.get_action()
|
||||||
|
self.assertEqual(action, Action.AUTH)
|
||||||
|
self.assertIsNotNone(connection.pk)
|
||||||
|
response = flow_manager.get_flow()
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
def test_unauthenticated_link(self):
|
def test_unauthenticated_link(self):
|
||||||
"""Test un-authenticated user linking"""
|
"""Test un-authenticated user linking"""
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
|
@ -31,6 +31,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": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -57,6 +58,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"name": uid,
|
"name": uid,
|
||||||
"authorization_flow": "",
|
"authorization_flow": "",
|
||||||
"invalidation_flow": "",
|
"invalidation_flow": "",
|
||||||
|
"redirect_uris": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -24,6 +24,7 @@ from rest_framework.fields import (
|
|||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.validators import UniqueValidator
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
@ -181,7 +182,10 @@ class CertificateDataSerializer(PassiveSerializer):
|
|||||||
class CertificateGenerationSerializer(PassiveSerializer):
|
class CertificateGenerationSerializer(PassiveSerializer):
|
||||||
"""Certificate generation parameters"""
|
"""Certificate generation parameters"""
|
||||||
|
|
||||||
common_name = CharField()
|
common_name = CharField(
|
||||||
|
validators=[UniqueValidator(queryset=CertificateKeyPair.objects.all())],
|
||||||
|
source="name",
|
||||||
|
)
|
||||||
subject_alt_name = CharField(required=False, allow_blank=True, label=_("Subject-alt name"))
|
subject_alt_name = CharField(required=False, allow_blank=True, label=_("Subject-alt name"))
|
||||||
validity_days = IntegerField(initial=365)
|
validity_days = IntegerField(initial=365)
|
||||||
alg = ChoiceField(default=PrivateKeyAlg.RSA, choices=PrivateKeyAlg.choices)
|
alg = ChoiceField(default=PrivateKeyAlg.RSA, choices=PrivateKeyAlg.choices)
|
||||||
@ -242,11 +246,10 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
|||||||
def generate(self, request: Request) -> Response:
|
def generate(self, request: Request) -> Response:
|
||||||
"""Generate a new, self-signed certificate-key pair"""
|
"""Generate a new, self-signed certificate-key pair"""
|
||||||
data = CertificateGenerationSerializer(data=request.data)
|
data = CertificateGenerationSerializer(data=request.data)
|
||||||
if not data.is_valid():
|
data.is_valid(raise_exception=True)
|
||||||
return Response(data.errors, status=400)
|
|
||||||
raw_san = data.validated_data.get("subject_alt_name", "")
|
raw_san = data.validated_data.get("subject_alt_name", "")
|
||||||
sans = raw_san.split(",") if raw_san != "" else []
|
sans = raw_san.split(",") if raw_san != "" else []
|
||||||
builder = CertificateBuilder(data.validated_data["common_name"])
|
builder = CertificateBuilder(data.validated_data["name"])
|
||||||
builder.alg = data.validated_data["alg"]
|
builder.alg = data.validated_data["alg"]
|
||||||
builder.build(
|
builder.build(
|
||||||
subject_alt_names=sans,
|
subject_alt_names=sans,
|
||||||
|
@ -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):
|
||||||
@ -89,6 +89,17 @@ class TestCrypto(APITestCase):
|
|||||||
self.assertIsInstance(ext[1], DNSName)
|
self.assertIsInstance(ext[1], DNSName)
|
||||||
self.assertEqual(ext[1].value, "baz")
|
self.assertEqual(ext[1].value, "baz")
|
||||||
|
|
||||||
|
def test_builder_api_duplicate(self):
|
||||||
|
"""Test Builder (via API)"""
|
||||||
|
cert = create_test_cert()
|
||||||
|
self.client.force_login(create_test_admin_user())
|
||||||
|
res = self.client.post(
|
||||||
|
reverse("authentik_api:certificatekeypair-generate"),
|
||||||
|
data={"common_name": cert.name, "subject_alt_name": "bar,baz", "validity_days": 3},
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 400)
|
||||||
|
self.assertJSONEqual(res.content, {"common_name": ["This field must be unique."]})
|
||||||
|
|
||||||
def test_builder_api_empty_san(self):
|
def test_builder_api_empty_san(self):
|
||||||
"""Test Builder (via API)"""
|
"""Test Builder (via API)"""
|
||||||
self.client.force_login(create_test_admin_user())
|
self.client.force_login(create_test_admin_user())
|
||||||
@ -263,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(
|
||||||
@ -295,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(
|
||||||
|
@ -16,13 +16,28 @@ class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RACProvider
|
model = RACProvider
|
||||||
fields = ProviderSerializer.Meta.fields + [
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"name",
|
||||||
|
"authentication_flow",
|
||||||
|
"authorization_flow",
|
||||||
|
"property_mappings",
|
||||||
|
"component",
|
||||||
|
"assigned_application_slug",
|
||||||
|
"assigned_application_name",
|
||||||
|
"assigned_backchannel_application_slug",
|
||||||
|
"assigned_backchannel_application_name",
|
||||||
|
"verbose_name",
|
||||||
|
"verbose_name_plural",
|
||||||
|
"meta_model_name",
|
||||||
"settings",
|
"settings",
|
||||||
"outpost_set",
|
"outpost_set",
|
||||||
"connection_expiry",
|
"connection_expiry",
|
||||||
"delete_token_on_disconnect",
|
"delete_token_on_disconnect",
|
||||||
]
|
]
|
||||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
extra_kwargs = {
|
||||||
|
"authorization_flow": {"required": True, "allow_null": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RACProviderViewSet(UsedByMixin, ModelViewSet):
|
class RACProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% load authentik_core %}
|
{% load authentik_core %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% versioned_script "dist/enterprise/rac/index-%v.js" %}
|
<script src="{% versioned_script 'dist/enterprise/rac/index-%v.js' %}" type="module"></script>
|
||||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||||
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||||
|
46
authentik/enterprise/providers/rac/tests/test_api.py
Normal file
46
authentik/enterprise/providers/rac/tests/test_api.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""Test RAC Provider"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from time import mktime
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
|
from authentik.enterprise.license import LicenseKey
|
||||||
|
from authentik.enterprise.models import License
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
|
class TestAPI(APITestCase):
|
||||||
|
"""Test Provider API"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = create_test_admin_user()
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"authentik.enterprise.license.LicenseKey.validate",
|
||||||
|
MagicMock(
|
||||||
|
return_value=LicenseKey(
|
||||||
|
aud="",
|
||||||
|
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
|
||||||
|
name=generate_id(),
|
||||||
|
internal_users=100,
|
||||||
|
external_users=100,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_create(self):
|
||||||
|
"""Test creation of RAC Provider"""
|
||||||
|
License.objects.create(key=generate_id())
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:racprovider-list"),
|
||||||
|
data={
|
||||||
|
"name": generate_id(),
|
||||||
|
"authorization_flow": create_test_flow().pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
@ -68,7 +68,6 @@ class TestEndpointsAPI(APITestCase):
|
|||||||
"name": self.provider.name,
|
"name": self.provider.name,
|
||||||
"authentication_flow": None,
|
"authentication_flow": None,
|
||||||
"authorization_flow": None,
|
"authorization_flow": None,
|
||||||
"invalidation_flow": None,
|
|
||||||
"property_mappings": [],
|
"property_mappings": [],
|
||||||
"connection_expiry": "hours=8",
|
"connection_expiry": "hours=8",
|
||||||
"delete_token_on_disconnect": False,
|
"delete_token_on_disconnect": False,
|
||||||
@ -121,7 +120,6 @@ class TestEndpointsAPI(APITestCase):
|
|||||||
"name": self.provider.name,
|
"name": self.provider.name,
|
||||||
"authentication_flow": None,
|
"authentication_flow": None,
|
||||||
"authorization_flow": None,
|
"authorization_flow": None,
|
||||||
"invalidation_flow": None,
|
|
||||||
"property_mappings": [],
|
"property_mappings": [],
|
||||||
"component": "ak-provider-rac-form",
|
"component": "ak-provider-rac-form",
|
||||||
"assigned_application_slug": self.app.slug,
|
"assigned_application_slug": self.app.slug,
|
||||||
@ -151,7 +149,6 @@ class TestEndpointsAPI(APITestCase):
|
|||||||
"name": self.provider.name,
|
"name": self.provider.name,
|
||||||
"authentication_flow": None,
|
"authentication_flow": None,
|
||||||
"authorization_flow": None,
|
"authorization_flow": None,
|
||||||
"invalidation_flow": None,
|
|
||||||
"property_mappings": [],
|
"property_mappings": [],
|
||||||
"component": "ak-provider-rac-form",
|
"component": "ak-provider-rac-form",
|
||||||
"assigned_application_slug": self.app.slug,
|
"assigned_application_slug": self.app.slug,
|
||||||
|
@ -18,7 +18,7 @@ window.authentik.flow = {
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% versioned_script "dist/flow/FlowInterface-%v.js" %}
|
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--ak-flow-background: url("{{ flow.background_url }}");
|
--ak-flow-background: url("{{ flow.background_url }}");
|
||||||
|
@ -89,6 +89,10 @@ class PasswordPolicy(Policy):
|
|||||||
|
|
||||||
def passes_static(self, password: str, request: PolicyRequest) -> PolicyResult:
|
def passes_static(self, password: str, request: PolicyRequest) -> PolicyResult:
|
||||||
"""Check static rules"""
|
"""Check static rules"""
|
||||||
|
error_message = self.error_message
|
||||||
|
if error_message == "":
|
||||||
|
error_message = _("Invalid password.")
|
||||||
|
|
||||||
if len(password) < self.length_min:
|
if len(password) < self.length_min:
|
||||||
LOGGER.debug("password failed", check="static", reason="length")
|
LOGGER.debug("password failed", check="static", reason="length")
|
||||||
return PolicyResult(False, self.error_message)
|
return PolicyResult(False, self.error_message)
|
||||||
|
@ -159,7 +159,10 @@ class LDAPOutpostConfigViewSet(ListModelMixin, GenericViewSet):
|
|||||||
access_response = PolicyResult(result.passing)
|
access_response = PolicyResult(result.passing)
|
||||||
response = self.LDAPCheckAccessSerializer(
|
response = self.LDAPCheckAccessSerializer(
|
||||||
instance={
|
instance={
|
||||||
"has_search_permission": request.user.has_perm("search_full_directory", provider),
|
"has_search_permission": (
|
||||||
|
request.user.has_perm("search_full_directory", provider)
|
||||||
|
or request.user.has_perm("authentik_providers_ldap.search_full_directory")
|
||||||
|
),
|
||||||
"access": access_response,
|
"access": access_response,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -11,13 +11,16 @@ class Migration(migrations.Migration):
|
|||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
# Original preserved
|
||||||
migrations.AddIndex(
|
# See https://github.com/goauthentik/authentik/issues/11874
|
||||||
model_name="accesstoken",
|
# operations = [
|
||||||
index=models.Index(fields=["token"], name="authentik_p_token_4bc870_idx"),
|
# migrations.AddIndex(
|
||||||
),
|
# model_name="accesstoken",
|
||||||
migrations.AddIndex(
|
# index=models.Index(fields=["token"], name="authentik_p_token_4bc870_idx"),
|
||||||
model_name="refreshtoken",
|
# ),
|
||||||
index=models.Index(fields=["token"], name="authentik_p_token_1a841f_idx"),
|
# migrations.AddIndex(
|
||||||
),
|
# model_name="refreshtoken",
|
||||||
]
|
# index=models.Index(fields=["token"], name="authentik_p_token_1a841f_idx"),
|
||||||
|
# ),
|
||||||
|
# ]
|
||||||
|
operations = []
|
||||||
|
@ -11,21 +11,24 @@ class Migration(migrations.Migration):
|
|||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
# Original preserved
|
||||||
migrations.RemoveIndex(
|
# See https://github.com/goauthentik/authentik/issues/11874
|
||||||
model_name="accesstoken",
|
# operations = [
|
||||||
name="authentik_p_token_4bc870_idx",
|
# migrations.RemoveIndex(
|
||||||
),
|
# model_name="accesstoken",
|
||||||
migrations.RemoveIndex(
|
# name="authentik_p_token_4bc870_idx",
|
||||||
model_name="refreshtoken",
|
# ),
|
||||||
name="authentik_p_token_1a841f_idx",
|
# migrations.RemoveIndex(
|
||||||
),
|
# model_name="refreshtoken",
|
||||||
migrations.AddIndex(
|
# name="authentik_p_token_1a841f_idx",
|
||||||
model_name="accesstoken",
|
# ),
|
||||||
index=models.Index(fields=["token", "provider"], name="authentik_p_token_f99422_idx"),
|
# migrations.AddIndex(
|
||||||
),
|
# model_name="accesstoken",
|
||||||
migrations.AddIndex(
|
# index=models.Index(fields=["token", "provider"], name="authentik_p_token_f99422_idx"),
|
||||||
model_name="refreshtoken",
|
# ),
|
||||||
index=models.Index(fields=["token", "provider"], name="authentik_p_token_a1d921_idx"),
|
# migrations.AddIndex(
|
||||||
),
|
# model_name="refreshtoken",
|
||||||
]
|
# index=models.Index(fields=["token", "provider"], name="authentik_p_token_a1d921_idx"),
|
||||||
|
# ),
|
||||||
|
# ]
|
||||||
|
operations = []
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-10-31 14:28
|
||||||
|
|
||||||
|
import django.contrib.postgres.indexes
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
|
||||||
|
("authentik_providers_oauth2", "0022_remove_accesstoken_session_id_and_more"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunSQL("DROP INDEX IF EXISTS authentik_p_token_f99422_idx;"),
|
||||||
|
migrations.RunSQL("DROP INDEX IF EXISTS authentik_p_token_a1d921_idx;"),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="accesstoken",
|
||||||
|
index=django.contrib.postgres.indexes.HashIndex(
|
||||||
|
fields=["token"], name="authentik_p_token_e00883_hash"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="refreshtoken",
|
||||||
|
index=django.contrib.postgres.indexes.HashIndex(
|
||||||
|
fields=["token"], name="authentik_p_token_32e2b7_hash"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -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,7 +12,9 @@ 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.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
@ -76,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."""
|
||||||
|
|
||||||
@ -155,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(
|
||||||
@ -270,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)
|
||||||
@ -418,7 +453,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["token", "provider"]),
|
HashIndex(fields=["token"]),
|
||||||
]
|
]
|
||||||
verbose_name = _("OAuth2 Access Token")
|
verbose_name = _("OAuth2 Access Token")
|
||||||
verbose_name_plural = _("OAuth2 Access Tokens")
|
verbose_name_plural = _("OAuth2 Access Tokens")
|
||||||
@ -464,7 +499,7 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["token", "provider"]),
|
HashIndex(fields=["token"]),
|
||||||
]
|
]
|
||||||
verbose_name = _("OAuth2 Refresh Token")
|
verbose_name = _("OAuth2 Refresh Token")
|
||||||
verbose_name_plural = _("OAuth2 Refresh Tokens")
|
verbose_name_plural = _("OAuth2 Refresh Tokens")
|
||||||
|
@ -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)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from authentik.core.models import Application, Group
|
from authentik.core.models import Application, Group
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||||
@ -34,7 +35,10 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
|||||||
self.brand.flow_device_code = self.device_flow
|
self.brand.flow_device_code = self.device_flow
|
||||||
self.brand.save()
|
self.brand.save()
|
||||||
|
|
||||||
def test_device_init(self):
|
self.api_client = APIClient()
|
||||||
|
self.api_client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_device_init_get(self):
|
||||||
"""Test device init"""
|
"""Test device init"""
|
||||||
res = self.client.get(reverse("authentik_providers_oauth2_root:device-login"))
|
res = self.client.get(reverse("authentik_providers_oauth2_root:device-login"))
|
||||||
self.assertEqual(res.status_code, 302)
|
self.assertEqual(res.status_code, 302)
|
||||||
@ -48,6 +52,76 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_device_init_post(self):
|
||||||
|
"""Test device init"""
|
||||||
|
res = self.api_client.get(reverse("authentik_providers_oauth2_root:device-login"))
|
||||||
|
self.assertEqual(res.status_code, 302)
|
||||||
|
self.assertEqual(
|
||||||
|
res.url,
|
||||||
|
reverse(
|
||||||
|
"authentik_core:if-flow",
|
||||||
|
kwargs={
|
||||||
|
"flow_slug": self.device_flow.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
res = self.api_client.get(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:flow-executor",
|
||||||
|
kwargs={
|
||||||
|
"flow_slug": self.device_flow.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
res.content,
|
||||||
|
{
|
||||||
|
"component": "ak-provider-oauth2-device-code",
|
||||||
|
"flow_info": {
|
||||||
|
"background": "/static/dist/assets/images/flow_background.jpg",
|
||||||
|
"cancel_url": "/flows/-/cancel/",
|
||||||
|
"layout": "stacked",
|
||||||
|
"title": self.device_flow.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = OAuth2Provider.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
authorization_flow=create_test_flow(),
|
||||||
|
)
|
||||||
|
Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||||
|
token = DeviceToken.objects.create(
|
||||||
|
provider=provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
res = self.api_client.post(
|
||||||
|
reverse(
|
||||||
|
"authentik_api:flow-executor",
|
||||||
|
kwargs={
|
||||||
|
"flow_slug": self.device_flow.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
data={
|
||||||
|
"component": "ak-provider-oauth2-device-code",
|
||||||
|
"code": token.user_code,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
res.content,
|
||||||
|
{
|
||||||
|
"component": "xak-flow-redirect",
|
||||||
|
"to": reverse(
|
||||||
|
"authentik_core:if-flow",
|
||||||
|
kwargs={
|
||||||
|
"flow_slug": provider.authorization_flow.slug,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_no_flow(self):
|
def test_no_flow(self):
|
||||||
"""Test no flow"""
|
"""Test no flow"""
|
||||||
self.brand.flow_device_code = None
|
self.brand.flow_device_code = None
|
||||||
|
@ -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 == "*":
|
|
||||||
LOGGER.info("Converting redirect_uris to regex", redirect=self.redirect_uri)
|
|
||||||
self.provider.redirect_uris = ".*"
|
|
||||||
self.provider.save()
|
|
||||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
if fullmatch(allowed.url, self.redirect_uri):
|
||||||
LOGGER.warning(
|
match_found = True
|
||||||
"Invalid redirect uri (regex comparison)",
|
break
|
||||||
redirect_uri_given=self.redirect_uri,
|
|
||||||
redirect_uri_expected=allowed_redirect_urls,
|
|
||||||
)
|
|
||||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
|
||||||
except RegexError as exc:
|
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(
|
LOGGER.warning(
|
||||||
"Invalid redirect uri (strict comparison)",
|
"Failed to parse regular expression",
|
||||||
redirect_uri_given=self.redirect_uri,
|
exc=exc,
|
||||||
redirect_uri_expected=allowed_redirect_urls,
|
url=allowed.url,
|
||||||
|
provider=self.provider,
|
||||||
)
|
)
|
||||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) from None
|
if not match_found:
|
||||||
|
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||||
# 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)
|
||||||
|
@ -5,7 +5,7 @@ from typing import Any
|
|||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField, IntegerField
|
from rest_framework.fields import CharField
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
@ -47,6 +47,9 @@ class CodeValidatorView(PolicyAccessView):
|
|||||||
self.provider = self.token.provider
|
self.provider = self.token.provider
|
||||||
self.application = self.token.provider.application
|
self.application = self.token.provider.application
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest, *args, **kwargs):
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs):
|
def get(self, request: HttpRequest, *args, **kwargs):
|
||||||
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
|
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
|
||||||
planner = FlowPlanner(self.provider.authorization_flow)
|
planner = FlowPlanner(self.provider.authorization_flow)
|
||||||
@ -122,7 +125,7 @@ class OAuthDeviceCodeChallenge(Challenge):
|
|||||||
class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
|
class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
|
||||||
"""Response that includes the user-entered device code"""
|
"""Response that includes the user-entered device code"""
|
||||||
|
|
||||||
code = IntegerField()
|
code = CharField()
|
||||||
component = CharField(default="ak-provider-oauth2-device-code")
|
component = CharField(default="ak-provider-oauth2-device-code")
|
||||||
|
|
||||||
def validate_code(self, code: int) -> HttpResponse | None:
|
def validate_code(self, code: int) -> HttpResponse | None:
|
||||||
|
@ -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):
|
||||||
|
@ -19,6 +19,7 @@ SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
|
|||||||
class User(BaseUser):
|
class User(BaseUser):
|
||||||
"""Modified User schema with added externalId field"""
|
"""Modified User schema with added externalId field"""
|
||||||
|
|
||||||
|
id: str | int | None = None
|
||||||
schemas: list[str] = [SCIM_USER_SCHEMA]
|
schemas: list[str] = [SCIM_USER_SCHEMA]
|
||||||
externalId: str | None = None
|
externalId: str | None = None
|
||||||
meta: dict | None = None
|
meta: dict | None = None
|
||||||
@ -27,6 +28,7 @@ class User(BaseUser):
|
|||||||
class Group(BaseGroup):
|
class Group(BaseGroup):
|
||||||
"""Modified Group schema with added externalId field"""
|
"""Modified Group schema with added externalId field"""
|
||||||
|
|
||||||
|
id: str | int | None = None
|
||||||
schemas: list[str] = [SCIM_GROUP_SCHEMA]
|
schemas: list[str] = [SCIM_GROUP_SCHEMA]
|
||||||
externalId: str | None = None
|
externalId: str | None = None
|
||||||
meta: dict | None = None
|
meta: dict | None = None
|
||||||
|
@ -53,7 +53,7 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
|
|||||||
except LookupError:
|
except LookupError:
|
||||||
return None
|
return None
|
||||||
objects = get_objects_for_group(instance.group, f"{app_label}.view_{model}", model_class)
|
objects = get_objects_for_group(instance.group, f"{app_label}.view_{model}", model_class)
|
||||||
obj = objects.first()
|
obj = objects.filter(pk=instance.object_pk).first()
|
||||||
if not obj:
|
if not obj:
|
||||||
return None
|
return None
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
@ -53,7 +53,7 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
|
|||||||
except LookupError:
|
except LookupError:
|
||||||
return None
|
return None
|
||||||
objects = get_objects_for_user(instance.user, f"{app_label}.view_{model}", model_class)
|
objects = get_objects_for_user(instance.user, f"{app_label}.view_{model}", model_class)
|
||||||
obj = objects.first()
|
obj = objects.filter(pk=instance.object_pk).first()
|
||||||
if not obj:
|
if not obj:
|
||||||
return None
|
return None
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ class CaptchaStageSerializer(StageSerializer):
|
|||||||
"private_key",
|
"private_key",
|
||||||
"js_url",
|
"js_url",
|
||||||
"api_url",
|
"api_url",
|
||||||
|
"interactive",
|
||||||
"score_min_threshold",
|
"score_min_threshold",
|
||||||
"score_max_threshold",
|
"score_max_threshold",
|
||||||
"error_on_invalid_score",
|
"error_on_invalid_score",
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-10-30 14:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_stages_captcha", "0003_captchastage_error_on_invalid_score_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="captchastage",
|
||||||
|
name="interactive",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@ -9,11 +9,13 @@ from authentik.flows.models import Stage
|
|||||||
|
|
||||||
|
|
||||||
class CaptchaStage(Stage):
|
class CaptchaStage(Stage):
|
||||||
"""Verify the user is human using Google's reCaptcha."""
|
"""Verify the user is human using Google's reCaptcha/other compatible CAPTCHA solutions."""
|
||||||
|
|
||||||
public_key = models.TextField(help_text=_("Public key, acquired your captcha Provider."))
|
public_key = models.TextField(help_text=_("Public key, acquired your captcha Provider."))
|
||||||
private_key = models.TextField(help_text=_("Private key, acquired your captcha Provider."))
|
private_key = models.TextField(help_text=_("Private key, acquired your captcha Provider."))
|
||||||
|
|
||||||
|
interactive = models.BooleanField(default=False)
|
||||||
|
|
||||||
score_min_threshold = models.FloatField(default=0.5) # Default values for reCaptcha
|
score_min_threshold = models.FloatField(default=0.5) # Default values for reCaptcha
|
||||||
score_max_threshold = models.FloatField(default=1.0) # Default values for reCaptcha
|
score_max_threshold = models.FloatField(default=1.0) # Default values for reCaptcha
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import BooleanField, CharField
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
@ -24,10 +24,12 @@ PLAN_CONTEXT_CAPTCHA = "captcha"
|
|||||||
class CaptchaChallenge(WithUserInfoChallenge):
|
class CaptchaChallenge(WithUserInfoChallenge):
|
||||||
"""Site public key"""
|
"""Site public key"""
|
||||||
|
|
||||||
site_key = CharField()
|
|
||||||
js_url = CharField()
|
|
||||||
component = CharField(default="ak-stage-captcha")
|
component = CharField(default="ak-stage-captcha")
|
||||||
|
|
||||||
|
site_key = CharField(required=True)
|
||||||
|
js_url = CharField(required=True)
|
||||||
|
interactive = BooleanField(required=True)
|
||||||
|
|
||||||
|
|
||||||
def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str):
|
def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str):
|
||||||
"""Validate captcha token"""
|
"""Validate captcha token"""
|
||||||
@ -103,6 +105,7 @@ class CaptchaStageView(ChallengeStageView):
|
|||||||
data={
|
data={
|
||||||
"js_url": self.executor.current_stage.js_url,
|
"js_url": self.executor.current_stage.js_url,
|
||||||
"site_key": self.executor.current_stage.public_key,
|
"site_key": self.executor.current_stage.public_key,
|
||||||
|
"interactive": self.executor.current_stage.interactive,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -223,6 +223,7 @@ class IdentificationStageView(ChallengeStageView):
|
|||||||
{
|
{
|
||||||
"js_url": current_stage.captcha_stage.js_url,
|
"js_url": current_stage.captcha_stage.js_url,
|
||||||
"site_key": current_stage.captcha_stage.public_key,
|
"site_key": current_stage.captcha_stage.public_key,
|
||||||
|
"interactive": current_stage.captcha_stage.interactive,
|
||||||
}
|
}
|
||||||
if current_stage.captcha_stage
|
if current_stage.captcha_stage
|
||||||
else None
|
else None
|
||||||
|
@ -21,7 +21,7 @@ from authentik.flows.challenge import (
|
|||||||
WithUserInfoChallenge,
|
WithUserInfoChallenge,
|
||||||
)
|
)
|
||||||
from authentik.flows.exceptions import StageInvalidException
|
from authentik.flows.exceptions import StageInvalidException
|
||||||
from authentik.flows.models import Flow, FlowDesignation, Stage
|
from authentik.flows.models import Flow, Stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.lib.utils.reflection import path_to_class
|
from authentik.lib.utils.reflection import path_to_class
|
||||||
@ -141,11 +141,11 @@ class PasswordStageView(ChallengeStageView):
|
|||||||
"allow_show_password": self.executor.current_stage.allow_show_password,
|
"allow_show_password": self.executor.current_stage.allow_show_password,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
|
recovery_flow: Flow | None = self.request.brand.flow_recovery
|
||||||
if recovery_flow.exists():
|
if recovery_flow:
|
||||||
recover_url = reverse(
|
recover_url = reverse(
|
||||||
"authentik_core:if-flow",
|
"authentik_core:if-flow",
|
||||||
kwargs={"flow_slug": recovery_flow.first().slug},
|
kwargs={"flow_slug": recovery_flow.slug},
|
||||||
)
|
)
|
||||||
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url)
|
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url)
|
||||||
return challenge
|
return challenge
|
||||||
|
@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
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_brand, create_test_flow
|
||||||
from authentik.flows.markers import StageMarker
|
from authentik.flows.markers import StageMarker
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
@ -57,6 +57,9 @@ class TestPasswordStage(FlowTestCase):
|
|||||||
def test_recovery_flow_link(self):
|
def test_recovery_flow_link(self):
|
||||||
"""Test link to the default recovery flow"""
|
"""Test link to the default recovery flow"""
|
||||||
flow = create_test_flow(designation=FlowDesignation.RECOVERY)
|
flow = create_test_flow(designation=FlowDesignation.RECOVERY)
|
||||||
|
brand = create_test_brand()
|
||||||
|
brand.flow_recovery = flow
|
||||||
|
brand.save()
|
||||||
|
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
session = self.client.session
|
session = self.client.session
|
||||||
|
@ -101,7 +101,6 @@ entries:
|
|||||||
- !KeyOf prompt-field-email
|
- !KeyOf prompt-field-email
|
||||||
- !KeyOf prompt-field-password
|
- !KeyOf prompt-field-password
|
||||||
- !KeyOf prompt-field-password-repeat
|
- !KeyOf prompt-field-password-repeat
|
||||||
validation_policies: []
|
|
||||||
id: stage-default-oobe-password
|
id: stage-default-oobe-password
|
||||||
identifiers:
|
identifiers:
|
||||||
name: stage-default-oobe-password
|
name: stage-default-oobe-password
|
||||||
|
@ -2,6 +2,17 @@ version: 1
|
|||||||
metadata:
|
metadata:
|
||||||
name: Default - Password change flow
|
name: Default - Password change flow
|
||||||
entries:
|
entries:
|
||||||
|
- attrs:
|
||||||
|
check_static_rules: true
|
||||||
|
check_zxcvbn: true
|
||||||
|
length_min: 8
|
||||||
|
password_field: password
|
||||||
|
zxcvbn_score_threshold: 2
|
||||||
|
error_message: Password needs to be 8 characters or longer.
|
||||||
|
identifiers:
|
||||||
|
name: default-password-change-password-policy
|
||||||
|
model: authentik_policies_password.passwordpolicy
|
||||||
|
id: default-password-change-password-policy
|
||||||
- attrs:
|
- attrs:
|
||||||
designation: stage_configuration
|
designation: stage_configuration
|
||||||
name: Change Password
|
name: Change Password
|
||||||
@ -39,6 +50,8 @@ entries:
|
|||||||
fields:
|
fields:
|
||||||
- !KeyOf prompt-field-password
|
- !KeyOf prompt-field-password
|
||||||
- !KeyOf prompt-field-password-repeat
|
- !KeyOf prompt-field-password-repeat
|
||||||
|
validation_policies:
|
||||||
|
- !KeyOf default-password-change-password-policy
|
||||||
identifiers:
|
identifiers:
|
||||||
name: default-password-change-prompt
|
name: default-password-change-prompt
|
||||||
id: default-password-change-prompt
|
id: default-password-change-prompt
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "authentik 2024.10.0 Blueprint schema",
|
"title": "authentik 2024.10.4 Blueprint schema",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"entries"
|
"entries"
|
||||||
@ -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": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"matching_mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Redirect URIs",
|
"enum": [
|
||||||
"description": "Enter each URI on a new line."
|
"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",
|
||||||
@ -6974,7 +6995,7 @@
|
|||||||
"spnego_server_name": {
|
"spnego_server_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Spnego server name",
|
"title": "Spnego server name",
|
||||||
"description": "Force the use of a specific server name for SPNEGO"
|
"description": "Force the use of a specific server name for SPNEGO. Must be in the form HTTP@hostname"
|
||||||
},
|
},
|
||||||
"spnego_keytab": {
|
"spnego_keytab": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -9781,6 +9802,10 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Api url"
|
"title": "Api url"
|
||||||
},
|
},
|
||||||
|
"interactive": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Interactive"
|
||||||
|
},
|
||||||
"score_min_threshold": {
|
"score_min_threshold": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"title": "Score min threshold"
|
"title": "Score min threshold"
|
||||||
@ -13383,12 +13408,6 @@
|
|||||||
"title": "Authorization flow",
|
"title": "Authorization flow",
|
||||||
"description": "Flow used when authorizing this provider."
|
"description": "Flow used when authorizing this provider."
|
||||||
},
|
},
|
||||||
"invalidation_flow": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid",
|
|
||||||
"title": "Invalidation flow",
|
|
||||||
"description": "Flow used ending the session from a provider."
|
|
||||||
},
|
|
||||||
"property_mappings": {
|
"property_mappings": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
@ -31,7 +31,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.0}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -52,7 +52,7 @@ services:
|
|||||||
- postgresql
|
- postgresql
|
||||||
- redis
|
- redis
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.0}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
2
go.mod
2
go.mod
@ -29,7 +29,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/wwt/guac v1.3.2
|
github.com/wwt/guac v1.3.2
|
||||||
goauthentik.io/api/v3 v3.2024100.1
|
goauthentik.io/api/v3 v3.2024083.13
|
||||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.23.0
|
golang.org/x/oauth2 v0.23.0
|
||||||
golang.org/x/sync v0.8.0
|
golang.org/x/sync v0.8.0
|
||||||
|
4
go.sum
4
go.sum
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
|||||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
goauthentik.io/api/v3 v3.2024100.1 h1:ve8xiaKOyUD5oCkNAsu1o3nc7aolt9bKTTR2qMI1iU4=
|
goauthentik.io/api/v3 v3.2024083.13 h1:xKh3feJYUeLw583zZ5ifgV0qjD37ZCOzgXPfbHQSbHM=
|
||||||
goauthentik.io/api/v3 v3.2024100.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
goauthentik.io/api/v3 v3.2024083.13/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2024.10.0"
|
const VERSION = "2024.10.4"
|
||||||
|
@ -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)
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
|
"goauthentik.io/internal/config"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
"goauthentik.io/internal/outpost/proxyv2/hs256"
|
"goauthentik.io/internal/outpost/proxyv2/hs256"
|
||||||
@ -121,6 +122,14 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
|
|||||||
bs := string(h.Sum([]byte(*p.ClientId)))
|
bs := string(h.Sum([]byte(*p.ClientId)))
|
||||||
sessionName := fmt.Sprintf("authentik_proxy_%s", bs[:8])
|
sessionName := fmt.Sprintf("authentik_proxy_%s", bs[:8])
|
||||||
|
|
||||||
|
// When HOST_BROWSER is set, use that as Host header for token requests to make the issuer match
|
||||||
|
// otherwise we use the internally configured authentik_host
|
||||||
|
tokenEndpointHost := server.API().Outpost.Config["authentik_host"].(string)
|
||||||
|
if config.Get().AuthentikHostBrowser != "" {
|
||||||
|
tokenEndpointHost = config.Get().AuthentikHostBrowser
|
||||||
|
}
|
||||||
|
publicHTTPClient := web.NewHostInterceptor(c, tokenEndpointHost)
|
||||||
|
|
||||||
a := &Application{
|
a := &Application{
|
||||||
Host: externalHost.Host,
|
Host: externalHost.Host,
|
||||||
log: muxLogger,
|
log: muxLogger,
|
||||||
@ -131,7 +140,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
|
|||||||
tokenVerifier: verifier,
|
tokenVerifier: verifier,
|
||||||
proxyConfig: p,
|
proxyConfig: p,
|
||||||
httpClient: c,
|
httpClient: c,
|
||||||
publicHostHTTPClient: web.NewHostInterceptor(c, server.API().Outpost.Config["authentik_host"].(string)),
|
publicHostHTTPClient: publicHTTPClient,
|
||||||
mux: mux,
|
mux: mux,
|
||||||
errorTemplates: templates.GetTemplates(),
|
errorTemplates: templates.GetTemplates(),
|
||||||
ak: server.API(),
|
ak: server.API(),
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -14,8 +14,10 @@ type hostInterceptor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t hostInterceptor) RoundTrip(r *http.Request) (*http.Response, error) {
|
func (t hostInterceptor) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.Host != t.host {
|
||||||
r.Host = t.host
|
r.Host = t.host
|
||||||
r.Header.Set("X-Forwarded-Proto", t.scheme)
|
r.Header.Set("X-Forwarded-Proto", t.scheme)
|
||||||
|
}
|
||||||
return t.inner.RoundTrip(r)
|
return t.inner.RoundTrip(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
@ -8,7 +8,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-10-23 16:39+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -2614,7 +2614,12 @@ msgid "Captcha Stages"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/stages/captcha/stage.py
|
#: authentik/stages/captcha/stage.py
|
||||||
msgid "Invalid captcha response. Retrying may solve this issue."
|
msgid "Unknown error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: authentik/stages/captcha/stage.py
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Failed to validate token: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/stages/captcha/stage.py
|
#: authentik/stages/captcha/stage.py
|
||||||
|
@ -11,17 +11,15 @@
|
|||||||
# Marco Vitale, 2024
|
# Marco Vitale, 2024
|
||||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2024
|
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2024
|
||||||
# albanobattistella <albanobattistella@gmail.com>, 2024
|
# albanobattistella <albanobattistella@gmail.com>, 2024
|
||||||
# Nicola Mersi, 2024
|
|
||||||
# tom max, 2024
|
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
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-10-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: albanobattistella <albanobattistella@gmail.com>, 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"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@ -585,28 +583,6 @@ msgstr "Limite massimo di connessioni raggiunto."
|
|||||||
msgid "(You are already connected in another tab/window)"
|
msgid "(You are already connected in another tab/window)"
|
||||||
msgstr "(Sei già connesso in un'altra scheda/finestra)"
|
msgstr "(Sei già connesso in un'altra scheda/finestra)"
|
||||||
|
|
||||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
|
||||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
|
||||||
msgstr ""
|
|
||||||
"Fase di autenticazione per la verifica dispositivo Google tramite endpoint"
|
|
||||||
|
|
||||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
|
||||||
msgid "Endpoint Authenticator Google Device Trust Connector Stages"
|
|
||||||
msgstr ""
|
|
||||||
"Fasi di autenticazione per la verifica dispositivo Google tramite endpoint"
|
|
||||||
|
|
||||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
|
||||||
msgid "Endpoint Device"
|
|
||||||
msgstr "Dispositivo di Accesso"
|
|
||||||
|
|
||||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
|
||||||
msgid "Endpoint Devices"
|
|
||||||
msgstr "Dispositivi di Accesso"
|
|
||||||
|
|
||||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py
|
|
||||||
msgid "Verifying your browser..."
|
|
||||||
msgstr "Verifica del tuo browser..."
|
|
||||||
|
|
||||||
#: authentik/enterprise/stages/source/models.py
|
#: authentik/enterprise/stages/source/models.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Amount of time a user can take to return from the source to continue the "
|
"Amount of time a user can take to return from the source to continue the "
|
||||||
@ -2041,124 +2017,6 @@ msgstr ""
|
|||||||
msgid "Used recovery-link to authenticate."
|
msgid "Used recovery-link to authenticate."
|
||||||
msgstr "Utilizzato il link di recupero per autenticarsi."
|
msgstr "Utilizzato il link di recupero per autenticarsi."
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Kerberos realm"
|
|
||||||
msgstr "Dominio Kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Custom krb5.conf to use. Uses the system one by default"
|
|
||||||
msgstr ""
|
|
||||||
"krb5.conf personalizzato da usare. Usa la configurazione di sistema per "
|
|
||||||
"default"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Sync users from Kerberos into authentik"
|
|
||||||
msgstr "Sincronizza utenti da Kerberos a authentik"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "When a user changes their password, sync it back to Kerberos"
|
|
||||||
msgstr "Quando un utente cambia la sua password, sincronizzala in Kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Principal to authenticate to kadmin for sync."
|
|
||||||
msgstr "Entità da autenticare su kadmin per la sincronizzazione."
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Password to authenticate to kadmin for sync"
|
|
||||||
msgstr "Password per autenticarsi in kadmin per sincronizzare"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid ""
|
|
||||||
"Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the "
|
|
||||||
"form TYPE:residual"
|
|
||||||
msgstr ""
|
|
||||||
"Keytab per autenticarsi su kadmin per la sincronizzazione. Deve essere con "
|
|
||||||
"codifica base64 o nel formato TYPE:residual"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid ""
|
|
||||||
"Credentials cache to authenticate to kadmin for sync. Must be in the form "
|
|
||||||
"TYPE:residual"
|
|
||||||
msgstr ""
|
|
||||||
"Credenziali memorizzate nella cache per autenticarsi su kadmin per la "
|
|
||||||
"sincronizzazione. Devono essere nel formato TYPE:residual"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid ""
|
|
||||||
"Force the use of a specific server name for SPNEGO. Must be in the form "
|
|
||||||
"HTTP@hostname"
|
|
||||||
msgstr ""
|
|
||||||
"Forza l'uso di un nome server specifico per SPNEGO. Deve essere nel formato "
|
|
||||||
"HTTP@nomehost"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"
|
|
||||||
msgstr ""
|
|
||||||
"keytab SPNEGO con codifica base64 o percorso del keytab nel formato "
|
|
||||||
"FILE:percorso"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Credential cache to use for SPNEGO in form type:residual"
|
|
||||||
msgstr ""
|
|
||||||
"Cache delle credenziali da utilizzare per SPNEGO nella forma type:residual"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid ""
|
|
||||||
"If enabled, the authentik-stored password will be updated upon login with "
|
|
||||||
"the Kerberos password backend"
|
|
||||||
msgstr ""
|
|
||||||
"Se abilitato, la password memorizzata in authentik verrà aggiornata al login"
|
|
||||||
" nel backend Kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Kerberos Source"
|
|
||||||
msgstr "Sorgente Kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Kerberos Sources"
|
|
||||||
msgstr "Sorgenti Kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Kerberos Source Property Mapping"
|
|
||||||
msgstr "Mappa delle proprietà della sorgente kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Kerberos Source Property Mappings"
|
|
||||||
msgstr "Mappe delle proprietà della sorgente kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "User Kerberos Source Connection"
|
|
||||||
msgstr "Connessione sorgente dell'utente kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "User Kerberos Source Connections"
|
|
||||||
msgstr " Connessioni alle sorgente dell'utente kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Group Kerberos Source Connection"
|
|
||||||
msgstr " Connessione sorgente del gruppo kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/models.py
|
|
||||||
msgid "Group Kerberos Source Connections"
|
|
||||||
msgstr "Connessioni alle sorgenti del gruppo kerberos"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/views.py
|
|
||||||
msgid "SPNEGO authentication required"
|
|
||||||
msgstr "autenticazione SPNEGO necessaria"
|
|
||||||
|
|
||||||
#: authentik/sources/kerberos/views.py
|
|
||||||
msgid ""
|
|
||||||
"\n"
|
|
||||||
" Make sure you have valid tickets (obtainable via kinit)\n"
|
|
||||||
" and configured the browser correctly.\n"
|
|
||||||
" Please contact your administrator.\n"
|
|
||||||
" "
|
|
||||||
msgstr ""
|
|
||||||
"\n"
|
|
||||||
"Assicurati di avere un ticket valido (ottenibile tramite kinit)\n"
|
|
||||||
" e di aver configurato correttamente il browser. \n"
|
|
||||||
"Contatta il tuo amministratore."
|
|
||||||
|
|
||||||
#: authentik/sources/ldap/api.py
|
#: authentik/sources/ldap/api.py
|
||||||
msgid "Only a single LDAP Source with password synchronization is allowed"
|
msgid "Only a single LDAP Source with password synchronization is allowed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2877,10 +2735,13 @@ msgid "Captcha Stages"
|
|||||||
msgstr "Fasi Captcha"
|
msgstr "Fasi Captcha"
|
||||||
|
|
||||||
#: authentik/stages/captcha/stage.py
|
#: authentik/stages/captcha/stage.py
|
||||||
msgid "Invalid captcha response. Retrying may solve this issue."
|
msgid "Unknown error"
|
||||||
msgstr ""
|
msgstr "Errore sconosciuto"
|
||||||
"Risposta captcha non valida. Un nuovo tentativo potrebbe risolvere il "
|
|
||||||
"problema."
|
#: authentik/stages/captcha/stage.py
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Failed to validate token: {error}"
|
||||||
|
msgstr "Impossibile convalidare il token: {error}"
|
||||||
|
|
||||||
#: authentik/stages/captcha/stage.py
|
#: authentik/stages/captcha/stage.py
|
||||||
msgid "Invalid captcha response"
|
msgid "Invalid captcha response"
|
||||||
@ -3253,10 +3114,6 @@ msgstr "Database utente + password app"
|
|||||||
msgid "User database + LDAP password"
|
msgid "User database + LDAP password"
|
||||||
msgstr "Database utenti + password LDAP"
|
msgstr "Database utenti + password LDAP"
|
||||||
|
|
||||||
#: authentik/stages/password/models.py
|
|
||||||
msgid "User database + Kerberos password"
|
|
||||||
msgstr "Database utenti + password Kerberos"
|
|
||||||
|
|
||||||
#: authentik/stages/password/models.py
|
#: authentik/stages/password/models.py
|
||||||
msgid "Selection of backends to test the password against."
|
msgid "Selection of backends to test the password against."
|
||||||
msgstr "Selezione di backend su cui testare la password."
|
msgstr "Selezione di backend su cui testare la password."
|
||||||
|
Binary file not shown.
@ -15,7 +15,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-10-23 16:39+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2024\n"
|
"Last-Translator: deluxghost, 2024\n"
|
||||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||||
@ -2649,8 +2649,13 @@ msgid "Captcha Stages"
|
|||||||
msgstr "验证码阶段"
|
msgstr "验证码阶段"
|
||||||
|
|
||||||
#: authentik/stages/captcha/stage.py
|
#: authentik/stages/captcha/stage.py
|
||||||
msgid "Invalid captcha response. Retrying may solve this issue."
|
msgid "Unknown error"
|
||||||
msgstr "无效的验证码响应。重试可能会解决此问题。"
|
msgstr "未知错误"
|
||||||
|
|
||||||
|
#: authentik/stages/captcha/stage.py
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Failed to validate token: {error}"
|
||||||
|
msgstr "验证令牌失败:{error}"
|
||||||
|
|
||||||
#: authentik/stages/captcha/stage.py
|
#: authentik/stages/captcha/stage.py
|
||||||
msgid "Invalid captcha response"
|
msgid "Invalid captcha response"
|
||||||
|
@ -14,7 +14,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-10-23 16:39+0000\n"
|
||||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2024\n"
|
"Last-Translator: deluxghost, 2024\n"
|
||||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||||
@ -2648,8 +2648,13 @@ msgid "Captcha Stages"
|
|||||||
msgstr "验证码阶段"
|
msgstr "验证码阶段"
|
||||||
|
|
||||||
#: authentik/stages/captcha/stage.py
|
#: authentik/stages/captcha/stage.py
|
||||||
msgid "Invalid captcha response. Retrying may solve this issue."
|
msgid "Unknown error"
|
||||||
msgstr "无效的验证码响应。重试可能会解决此问题。"
|
msgstr "未知错误"
|
||||||
|
|
||||||
|
#: authentik/stages/captcha/stage.py
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Failed to validate token: {error}"
|
||||||
|
msgstr "验证令牌失败:{error}"
|
||||||
|
|
||||||
#: authentik/stages/captcha/stage.py
|
#: authentik/stages/captcha/stage.py
|
||||||
msgid "Invalid captcha response"
|
msgid "Invalid captcha response"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2024.10.0",
|
"version": "2024.10.4",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
28
poetry.lock
generated
28
poetry.lock
generated
@ -3896,13 +3896,13 @@ pytest = ">=4.0.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-randomly"
|
name = "pytest-randomly"
|
||||||
version = "3.16.0"
|
version = "3.15.0"
|
||||||
description = "Pytest plugin to randomly order tests and control random.seed."
|
description = "Pytest plugin to randomly order tests and control random.seed."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"},
|
{file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"},
|
||||||
{file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"},
|
{file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -4354,13 +4354,13 @@ django-query = ["django (>=3.2)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "selenium"
|
name = "selenium"
|
||||||
version = "4.26.0"
|
version = "4.25.0"
|
||||||
description = "Official Python bindings for Selenium WebDriver"
|
description = "Official Python bindings for Selenium WebDriver"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "selenium-4.26.0-py3-none-any.whl", hash = "sha256:48013f36e812de5b3948ef53d04e73f77bc923ee3e1d7d99eaf0618179081b99"},
|
{file = "selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33"},
|
||||||
{file = "selenium-4.26.0.tar.gz", hash = "sha256:f0780f85f10310aa5d085b81e79d73d3c93b83d8de121d0400d543a50ee963e8"},
|
{file = "selenium-4.25.0.tar.gz", hash = "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -4425,13 +4425,13 @@ tornado = ["tornado (>=6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "service-identity"
|
name = "service-identity"
|
||||||
version = "24.2.0"
|
version = "24.1.0"
|
||||||
description = "Service identity verification for pyOpenSSL & cryptography."
|
description = "Service identity verification for pyOpenSSL & cryptography."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85"},
|
{file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"},
|
||||||
{file = "service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09"},
|
{file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -4441,7 +4441,7 @@ pyasn1 = "*"
|
|||||||
pyasn1-modules = "*"
|
pyasn1-modules = "*"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["coverage[toml] (>=5.0.2)", "idna", "mypy", "pyopenssl", "pytest", "types-pyopenssl"]
|
dev = ["pyopenssl", "service-identity[idna,mypy,tests]"]
|
||||||
docs = ["furo", "myst-parser", "pyopenssl", "sphinx", "sphinx-notfound-page"]
|
docs = ["furo", "myst-parser", "pyopenssl", "sphinx", "sphinx-notfound-page"]
|
||||||
idna = ["idna"]
|
idna = ["idna"]
|
||||||
mypy = ["idna", "mypy", "types-pyopenssl"]
|
mypy = ["idna", "mypy", "types-pyopenssl"]
|
||||||
@ -4751,13 +4751,13 @@ wsproto = ">=0.14"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "twilio"
|
name = "twilio"
|
||||||
version = "9.3.6"
|
version = "9.3.5"
|
||||||
description = "Twilio API client and TwiML generator"
|
description = "Twilio API client and TwiML generator"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.0"
|
python-versions = ">=3.7.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "twilio-9.3.6-py2.py3-none-any.whl", hash = "sha256:c5d7f4cfeb50a7928397b8f819c8f7fb2bb956a1a2cabbda1df1d7a40f9ce1d7"},
|
{file = "twilio-9.3.5-py2.py3-none-any.whl", hash = "sha256:d6a97a77b98cc176a61c960f11894af385bc1c11b93e2e8b79fdfb9601788fb0"},
|
||||||
{file = "twilio-9.3.6.tar.gz", hash = "sha256:d42691f7fe1faaa5ba82942f169bfea4d7f01a0a542a456d82018fb49bd1f5b2"},
|
{file = "twilio-9.3.5.tar.gz", hash = "sha256:608d78a903d403465aac1840c58a6546a090b7e222d2bf539a93c3831072880c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2024.10.0"
|
version = "2024.10.4"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||||
|
|
||||||
|
94
schema.yml
94
schema.yml
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2024.10.0
|
version: 2024.10.4
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
@ -20218,10 +20218,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:
|
||||||
@ -20637,10 +20633,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
|
||||||
@ -39220,7 +39212,10 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
js_url:
|
js_url:
|
||||||
type: string
|
type: string
|
||||||
|
interactive:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
|
- interactive
|
||||||
- js_url
|
- js_url
|
||||||
- pending_user
|
- pending_user
|
||||||
- pending_user_avatar
|
- pending_user_avatar
|
||||||
@ -39276,6 +39271,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
api_url:
|
api_url:
|
||||||
type: string
|
type: string
|
||||||
|
interactive:
|
||||||
|
type: boolean
|
||||||
score_min_threshold:
|
score_min_threshold:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
@ -39322,6 +39319,8 @@ components:
|
|||||||
api_url:
|
api_url:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
interactive:
|
||||||
|
type: boolean
|
||||||
score_min_threshold:
|
score_min_threshold:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
@ -42975,7 +42974,8 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
spnego_server_name:
|
spnego_server_name:
|
||||||
type: string
|
type: string
|
||||||
description: Force the use of a specific server name for SPNEGO
|
description: Force the use of a specific server name for SPNEGO. Must be
|
||||||
|
in the form HTTP@hostname
|
||||||
spnego_ccache:
|
spnego_ccache:
|
||||||
type: string
|
type: string
|
||||||
description: Credential cache to use for SPNEGO in form type:residual
|
description: Credential cache to use for SPNEGO in form type:residual
|
||||||
@ -43144,7 +43144,8 @@ components:
|
|||||||
be in the form TYPE:residual
|
be in the form TYPE:residual
|
||||||
spnego_server_name:
|
spnego_server_name:
|
||||||
type: string
|
type: string
|
||||||
description: Force the use of a specific server name for SPNEGO
|
description: Force the use of a specific server name for SPNEGO. Must be
|
||||||
|
in the form HTTP@hostname
|
||||||
spnego_keytab:
|
spnego_keytab:
|
||||||
type: string
|
type: string
|
||||||
writeOnly: true
|
writeOnly: true
|
||||||
@ -44051,6 +44052,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
|
||||||
@ -44753,8 +44759,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'
|
||||||
@ -44783,6 +44790,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:
|
||||||
@ -44854,8 +44862,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'
|
||||||
@ -44877,6 +44886,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
|
||||||
@ -44934,7 +44944,8 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
default: ak-provider-oauth2-device-code
|
default: ak-provider-oauth2-device-code
|
||||||
code:
|
code:
|
||||||
type: integer
|
type: string
|
||||||
|
minLength: 1
|
||||||
required:
|
required:
|
||||||
- code
|
- code
|
||||||
OAuthDeviceCodeFinishChallenge:
|
OAuthDeviceCodeFinishChallenge:
|
||||||
@ -47730,6 +47741,8 @@ components:
|
|||||||
api_url:
|
api_url:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
interactive:
|
||||||
|
type: boolean
|
||||||
score_min_threshold:
|
score_min_threshold:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
@ -48448,7 +48461,8 @@ components:
|
|||||||
be in the form TYPE:residual
|
be in the form TYPE:residual
|
||||||
spnego_server_name:
|
spnego_server_name:
|
||||||
type: string
|
type: string
|
||||||
description: Force the use of a specific server name for SPNEGO
|
description: Force the use of a specific server name for SPNEGO. Must be
|
||||||
|
in the form HTTP@hostname
|
||||||
spnego_keytab:
|
spnego_keytab:
|
||||||
type: string
|
type: string
|
||||||
writeOnly: true
|
writeOnly: true
|
||||||
@ -48871,8 +48885,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'
|
||||||
@ -49461,10 +49476,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Flow used when authorizing this provider.
|
description: Flow used when authorizing this provider.
|
||||||
invalidation_flow:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: Flow used ending the session from a provider.
|
|
||||||
property_mappings:
|
property_mappings:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -51469,7 +51480,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
|
||||||
@ -51696,10 +51709,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Flow used when authorizing this provider.
|
description: Flow used when authorizing this provider.
|
||||||
invalidation_flow:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: Flow used ending the session from a provider.
|
|
||||||
property_mappings:
|
property_mappings:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -51757,7 +51766,6 @@ components:
|
|||||||
- assigned_backchannel_application_slug
|
- assigned_backchannel_application_slug
|
||||||
- authorization_flow
|
- authorization_flow
|
||||||
- component
|
- component
|
||||||
- invalidation_flow
|
|
||||||
- meta_model_name
|
- meta_model_name
|
||||||
- name
|
- name
|
||||||
- outpost_set
|
- outpost_set
|
||||||
@ -51781,10 +51789,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Flow used when authorizing this provider.
|
description: Flow used when authorizing this provider.
|
||||||
invalidation_flow:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: Flow used ending the session from a provider.
|
|
||||||
property_mappings:
|
property_mappings:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -51801,7 +51805,6 @@ components:
|
|||||||
description: When set to true, connection tokens will be deleted upon disconnect.
|
description: When set to true, connection tokens will be deleted upon disconnect.
|
||||||
required:
|
required:
|
||||||
- authorization_flow
|
- authorization_flow
|
||||||
- invalidation_flow
|
|
||||||
- name
|
- name
|
||||||
RadiusCheckAccess:
|
RadiusCheckAccess:
|
||||||
type: object
|
type: object
|
||||||
@ -52075,6 +52078,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(
|
||||||
|
@ -66,7 +66,6 @@ const rawCssImportMaps = [
|
|||||||
'import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";',
|
'import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";',
|
||||||
'import PFTable from "@patternfly/patternfly/components/Table/table.css";',
|
'import PFTable from "@patternfly/patternfly/components/Table/table.css";',
|
||||||
'import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";',
|
'import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";',
|
||||||
'import PFText from "@patternfly/patternfly/utilities/Text/text.css";',
|
|
||||||
'import PFTitle from "@patternfly/patternfly/components/Title/title.css";',
|
'import PFTitle from "@patternfly/patternfly/components/Title/title.css";',
|
||||||
'import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";',
|
'import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";',
|
||||||
'import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";',
|
'import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";',
|
||||||
|
@ -119,13 +119,22 @@ async function buildOneSource(source, dest) {
|
|||||||
Date.now() - start
|
Date.now() - start
|
||||||
}ms`,
|
}ms`,
|
||||||
);
|
);
|
||||||
|
return 0;
|
||||||
} catch (exc) {
|
} catch (exc) {
|
||||||
console.error(`[${new Date(Date.now()).toISOString()}] Failed to build ${source}: ${exc}`);
|
console.error(`[${new Date(Date.now()).toISOString()}] Failed to build ${source}: ${exc}`);
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildAuthentik(interfaces) {
|
async function buildAuthentik(interfaces) {
|
||||||
await Promise.allSettled(interfaces.map(([source, dest]) => buildOneSource(source, dest)));
|
const code = await Promise.allSettled(
|
||||||
|
interfaces.map(([source, dest]) => buildOneSource(source, dest)),
|
||||||
|
);
|
||||||
|
const finalCode = code.reduce((a, res) => a + res.value, 0);
|
||||||
|
if (finalCode > 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
@ -163,11 +172,12 @@ if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] ===
|
|||||||
});
|
});
|
||||||
} else if (process.argv.length > 2 && (process.argv[2] === "-p" || process.argv[2] === "--proxy")) {
|
} else if (process.argv.length > 2 && (process.argv[2] === "-p" || process.argv[2] === "--proxy")) {
|
||||||
// There's no watch-for-proxy, sorry.
|
// There's no watch-for-proxy, sorry.
|
||||||
|
process.exit(
|
||||||
await buildAuthentik(
|
await buildAuthentik(
|
||||||
interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
process.exit(0);
|
|
||||||
} else {
|
} else {
|
||||||
// And the fallback: just build it.
|
// And the fallback: just build it.
|
||||||
await buildAuthentik(interfaces);
|
process.exit(await buildAuthentik(interfaces));
|
||||||
}
|
}
|
||||||
|
349
web/package-lock.json
generated
349
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.0-1730331602",
|
"@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",
|
||||||
@ -1014,9 +1014,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@codemirror/commands": {
|
"node_modules/@codemirror/commands": {
|
||||||
"version": "6.7.0",
|
"version": "6.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.2.tgz",
|
||||||
"integrity": "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw==",
|
"integrity": "sha512-Fq7eWOl1Rcbrfn6jD8FPCj9Auaxdm5nIK5RYOeW7ughnd/rY5AmPg6b+CfsG39ZHdwiwe8lde3q8uR7CF5S0yQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.0.0",
|
"@codemirror/language": "^6.0.0",
|
||||||
"@codemirror/state": "^6.4.0",
|
"@codemirror/state": "^6.4.0",
|
||||||
@ -1775,9 +1775,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/api": {
|
"node_modules/@goauthentik/api": {
|
||||||
"version": "2024.10.0-1730331602",
|
"version": "2024.10.2-1732206118",
|
||||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.10.0-1730331602.tgz",
|
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.10.2-1732206118.tgz",
|
||||||
"integrity": "sha512-VaXywdDCFkIs9RgmHVYt8jGf5xnc+czsu5ILEThNQOuXvBjkGa0J8aPWVSdfP++GiHnkjddWVFzJ6R6LOoHbWQ=="
|
"integrity": "sha512-Zg90AJvGDquD3u73yIBKXFBDxsCljPxVqylylS6hgPzkLSogKVVkjhmKteWFXDrVxxsxo5XIa4FuTe3wAERyzw=="
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/web": {
|
"node_modules/@goauthentik/web": {
|
||||||
"resolved": "",
|
"resolved": "",
|
||||||
@ -1953,9 +1953,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@inquirer/figures": {
|
"node_modules/@inquirer/figures": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz",
|
||||||
"integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==",
|
"integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@ -2425,9 +2425,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@lezer/javascript": {
|
"node_modules/@lezer/javascript": {
|
||||||
"version": "1.4.19",
|
"version": "1.4.18",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.18.tgz",
|
||||||
"integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==",
|
"integrity": "sha512-Y8BeHOt4LtcxJgXwadtfSeWPrh0XzklcCHnCVT+vOsxqH4gWmunP2ykX+VVOlM/dusyVyiNfG3lv0f10UK+mgA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.2.0",
|
"@lezer/common": "^1.2.0",
|
||||||
"@lezer/highlight": "^1.1.3",
|
"@lezer/highlight": "^1.1.3",
|
||||||
@ -2507,9 +2507,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@lit/context": {
|
"node_modules/@lit/context": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.2.tgz",
|
||||||
"integrity": "sha512-Auh37F4S0PZM93HTDfZWs97mmzaQ7M3vnTc9YvxAGyP3UItSK/8Fs0vTOGT+njuvOwbKio/l8Cx/zWL4vkutpQ==",
|
"integrity": "sha512-S0nw2C6Tkm7fVX5TGYqeROGD+Z9Coa2iFpW+ysYBDH3YvCqOY3wVQvSgwbaliLJkjTnSEYCBe9qFqKV8WUFpVw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lit/reactive-element": "^1.6.2 || ^2.0.0"
|
"@lit/reactive-element": "^1.6.2 || ^2.0.0"
|
||||||
}
|
}
|
||||||
@ -3517,6 +3517,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
|
"hasInstallScript": true
|
||||||
|
},
|
||||||
"node_modules/@sec-ant/readable-stream": {
|
"node_modules/@sec-ant/readable-stream": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||||
@ -3673,9 +3679,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@spotlightjs/overlay": {
|
"node_modules/@spotlightjs/overlay": {
|
||||||
"version": "2.6.0",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@spotlightjs/overlay/-/overlay-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@spotlightjs/overlay/-/overlay-2.5.2.tgz",
|
||||||
"integrity": "sha512-UnvWi6J8MTxhUHaBxd+k2mVyQh+0sJ5MImb/UfnEOA0WhNaFl2Foo/R7ByoOIDzDGT42+mZtfEyhJzNZ5naAaQ=="
|
"integrity": "sha512-2YCxkTCRWn9Q4x6Aan0b43EvUJEgQBLvcws1c97LO7Cgnb0iKjiLIRW0Fa8f6PO8r9AcDvaFzhk5Z93YIkwlGQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@spotlightjs/sidecar": {
|
"node_modules/@spotlightjs/sidecar": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
@ -3694,11 +3700,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@spotlightjs/spotlight": {
|
"node_modules/@spotlightjs/spotlight": {
|
||||||
"version": "2.5.0",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@spotlightjs/spotlight/-/spotlight-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@spotlightjs/spotlight/-/spotlight-2.4.2.tgz",
|
||||||
"integrity": "sha512-tOlefTjOUuNGyHtt1+r1IdY5vq9Hq9Vi2HqPENZ9orS/12KMEK6rVFliWT0/IXJLH5LdunNGaw3kan08bsA+NQ==",
|
"integrity": "sha512-d7wEvG6fyP9OsqgJ3fD2YZ2zjtD9MzIuOpqTg+LAT1FXl7yBK9LRrLW/LU6OvwRdDCzUsbnxUv0giJ5DjV+2Lw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@spotlightjs/overlay": "2.6.0",
|
"@spotlightjs/overlay": "2.5.2",
|
||||||
"@spotlightjs/sidecar": "1.8.0",
|
"@spotlightjs/sidecar": "1.8.0",
|
||||||
"import-meta-resolve": "^4.1.0"
|
"import-meta-resolve": "^4.1.0"
|
||||||
},
|
},
|
||||||
@ -5879,9 +5885,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/mocha": {
|
"node_modules/@types/mocha": {
|
||||||
"version": "10.0.9",
|
"version": "10.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.8.tgz",
|
||||||
"integrity": "sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==",
|
"integrity": "sha512-HfMcUmy9hTMJh66VNcmeC9iVErIZJli2bszuXc6julh5YGuRb/W5OnkHjwLNYdFlMis0sY3If5SEAp+PktdJjw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/mute-stream": {
|
"node_modules/@types/mute-stream": {
|
||||||
@ -5894,9 +5900,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.7.5",
|
"version": "22.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
|
||||||
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -6077,16 +6083,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz",
|
||||||
"integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==",
|
"integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.8.1",
|
"@typescript-eslint/scope-manager": "8.8.0",
|
||||||
"@typescript-eslint/type-utils": "8.8.1",
|
"@typescript-eslint/type-utils": "8.8.0",
|
||||||
"@typescript-eslint/utils": "8.8.1",
|
"@typescript-eslint/utils": "8.8.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.8.1",
|
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.3.1",
|
"ignore": "^5.3.1",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@ -6110,15 +6116,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz",
|
||||||
"integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==",
|
"integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.8.1",
|
"@typescript-eslint/scope-manager": "8.8.0",
|
||||||
"@typescript-eslint/types": "8.8.1",
|
"@typescript-eslint/types": "8.8.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.8.1",
|
"@typescript-eslint/typescript-estree": "8.8.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.8.1",
|
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -6138,13 +6144,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz",
|
||||||
"integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==",
|
"integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.8.1",
|
"@typescript-eslint/types": "8.8.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.8.1"
|
"@typescript-eslint/visitor-keys": "8.8.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@ -6155,13 +6161,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz",
|
||||||
"integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==",
|
"integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.8.1",
|
"@typescript-eslint/typescript-estree": "8.8.0",
|
||||||
"@typescript-eslint/utils": "8.8.1",
|
"@typescript-eslint/utils": "8.8.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^1.3.0"
|
"ts-api-utils": "^1.3.0"
|
||||||
},
|
},
|
||||||
@ -6179,9 +6185,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz",
|
||||||
"integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==",
|
"integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@ -6192,13 +6198,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz",
|
||||||
"integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==",
|
"integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.8.1",
|
"@typescript-eslint/types": "8.8.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.8.1",
|
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@ -6220,15 +6226,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz",
|
||||||
"integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==",
|
"integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@typescript-eslint/scope-manager": "8.8.1",
|
"@typescript-eslint/scope-manager": "8.8.0",
|
||||||
"@typescript-eslint/types": "8.8.1",
|
"@typescript-eslint/types": "8.8.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.8.1"
|
"@typescript-eslint/typescript-estree": "8.8.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@ -6242,12 +6248,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz",
|
||||||
"integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==",
|
"integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.8.1",
|
"@typescript-eslint/types": "8.8.0",
|
||||||
"eslint-visitor-keys": "^3.4.3"
|
"eslint-visitor-keys": "^3.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -6843,9 +6849,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/cli/node_modules/@types/node": {
|
"node_modules/@wdio/cli/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -6971,9 +6977,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/local-runner/node_modules/@types/node": {
|
"node_modules/@wdio/local-runner/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -7024,9 +7030,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/mocha-framework/node_modules/@types/node": {
|
"node_modules/@wdio/mocha-framework/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -7051,9 +7057,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/repl/node_modules/@types/node": {
|
"node_modules/@wdio/repl/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -7076,9 +7082,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/reporter/node_modules/@types/node": {
|
"node_modules/@wdio/reporter/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -7107,9 +7113,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/runner/node_modules/@types/node": {
|
"node_modules/@wdio/runner/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -7156,9 +7162,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/types/node_modules/@types/node": {
|
"node_modules/@wdio/types/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -8693,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": {
|
||||||
@ -9005,9 +9011,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/confbox": {
|
"node_modules/confbox": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz",
|
||||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="
|
"integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA=="
|
||||||
},
|
},
|
||||||
"node_modules/consola": {
|
"node_modules/consola": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
@ -9052,9 +9058,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -9855,9 +9861,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/deepmerge-ts": {
|
"node_modules/deepmerge-ts": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.1.tgz",
|
||||||
"integrity": "sha512-qCSH6I0INPxd9Y1VtAiLpnYvz5O//6rCfJXKk0z66Up9/VOSr+1yS8XSKA5IWRxjocFGlzPyaZYe+jxq7OOLtQ==",
|
"integrity": "sha512-M27OAbyR/XgJujhAd6ZlYvZGzejbzvGPSZWwuzezPCdKLT9VMtK0kpRNDc5LeUDYqFN3e254gWG1yKpjidCtow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
@ -10272,9 +10278,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.33",
|
"version": "1.5.32",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz",
|
||||||
"integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==",
|
"integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
@ -11032,16 +11038,16 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-wc": {
|
"node_modules/eslint-plugin-wc": {
|
||||||
"version": "2.2.0",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-2.1.1.tgz",
|
||||||
"integrity": "sha512-kjPp+aXz23fOl0JZJOJS+6adwhEv98KjZ2FJqWpc4vtmk4Oenz/JJmmNZrGSARgtyR0BLIF/kVWC6GSlHA+5MA==",
|
"integrity": "sha512-GfJo05ZgWfwAFbW6Gkf+9CMOIU6fmbd3b4nm+PKESHgUdUTmi7vawlELCrzOhdiQjXUPZxDfFIVxYt9D/v/GdQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-valid-element-name": "^1.0.0",
|
"is-valid-element-name": "^1.0.0",
|
||||||
"js-levenshtein-esm": "^1.2.0"
|
"js-levenshtein-esm": "^1.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=8.40.0"
|
"eslint": ">=5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
@ -11405,9 +11411,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.0",
|
"version": "4.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
@ -11415,7 +11421,7 @@
|
|||||||
"body-parser": "1.20.3",
|
"body-parser": "1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.6.0",
|
"cookie": "0.7.1",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
@ -14293,9 +14299,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/knip": {
|
"node_modules/knip": {
|
||||||
"version": "5.33.1",
|
"version": "5.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.33.1.tgz",
|
"resolved": "https://registry.npmjs.org/knip/-/knip-5.31.0.tgz",
|
||||||
"integrity": "sha512-SeuH+6IcDNNFRdVsi2uGnO6gsSDlx1V+TcQkKnzciF2Z7QHHasKseGUf9GMNVm3bSCKDeqaKPMp0F6BOiKuYRA==",
|
"integrity": "sha512-4hR+qHx/id7mniCWWUqA4MXwGjYFN75xv3qLmEkl9Hm6eCKAhv0wGP0CyrXKUYxVyDplJQsqQaAlsjuRKYsdPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -14317,7 +14323,7 @@
|
|||||||
"easy-table": "1.2.0",
|
"easy-table": "1.2.0",
|
||||||
"enhanced-resolve": "^5.17.1",
|
"enhanced-resolve": "^5.17.1",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"jiti": "^2.3.3",
|
"jiti": "^2.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
@ -14342,9 +14348,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/knip/node_modules/jiti": {
|
"node_modules/knip/node_modules/jiti": {
|
||||||
"version": "2.3.3",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.2.1.tgz",
|
||||||
"integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==",
|
"integrity": "sha512-weIl/Bv3G0J3UKamLxEA2G+FfQ33Z1ZkQJGPjKFV21zQdKWu2Pi6o4elpj2uEl5XdFJZ9xzn1fsanWTFSt45zw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
@ -14489,9 +14495,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/lit": {
|
"node_modules/lit": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz",
|
||||||
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
|
"integrity": "sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lit/reactive-element": "^2.0.4",
|
"@lit/reactive-element": "^2.0.4",
|
||||||
"lit-element": "^4.1.0",
|
"lit-element": "^4.1.0",
|
||||||
@ -14525,9 +14531,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/lit-element": {
|
"node_modules/lit-element": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.0.tgz",
|
||||||
"integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==",
|
"integrity": "sha512-gSejRUQJuMQjV2Z59KAS/D4iElUhwKpIyJvZ9w+DIagIQjfJnhR20h2Q5ddpzXGS+fF0tMZ/xEYGMnKmaI/iww==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lit-labs/ssr-dom-shim": "^1.2.0",
|
"@lit-labs/ssr-dom-shim": "^1.2.0",
|
||||||
"@lit/reactive-element": "^2.0.4",
|
"@lit/reactive-element": "^2.0.4",
|
||||||
@ -14535,9 +14541,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lit-html": {
|
"node_modules/lit-html": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.0.tgz",
|
||||||
"integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==",
|
"integrity": "sha512-pwT/HwoxqI9FggTrYVarkBKFN9MlTUpLrDHubTmW4SrkL3kkqW5gxwbxMMUnbbRHBC0WTZnYHcjDSCM559VyfA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/trusted-types": "^2.0.2"
|
"@types/trusted-types": "^2.0.2"
|
||||||
}
|
}
|
||||||
@ -15201,14 +15207,14 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/mlly": {
|
"node_modules/mlly": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz",
|
||||||
"integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==",
|
"integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.12.1",
|
"acorn": "^8.11.3",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"pkg-types": "^1.2.0",
|
"pkg-types": "^1.1.1",
|
||||||
"ufo": "^1.5.4"
|
"ufo": "^1.5.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mocha": {
|
"node_modules/mocha": {
|
||||||
@ -16586,9 +16592,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/package-manager-detector": {
|
"node_modules/package-manager-detector": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.0.tgz",
|
||||||
"integrity": "sha512-/hVW2fZvAdEas+wyKh0SnlZ2mx0NIa1+j11YaQkogEJkcMErbwchHCuo8z7lEtajZJQZ6rgZNVTWMVVd71Bjng=="
|
"integrity": "sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog=="
|
||||||
},
|
},
|
||||||
"node_modules/pako": {
|
"node_modules/pako": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
@ -16900,12 +16906,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkg-types": {
|
"node_modules/pkg-types": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz",
|
||||||
"integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==",
|
"integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"confbox": "^0.1.8",
|
"confbox": "^0.1.7",
|
||||||
"mlly": "^1.7.2",
|
"mlly": "^1.7.1",
|
||||||
"pathe": "^1.1.2"
|
"pathe": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -19538,11 +19544,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/swagger-client": {
|
"node_modules/swagger-client": {
|
||||||
"version": "3.29.4",
|
"version": "3.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.31.0.tgz",
|
||||||
"integrity": "sha512-Me8tdPyRAQbnwNBCZ0BpG0vyci9e+FW6YV3+c6/x8SwPmLpslpFNXoT4PtVApf1CVSvV7Sc7Bfb4DPgpEqBdHw==",
|
"integrity": "sha512-hVYift5XB8nOgNJVl6cbNtVTVPT2Fdx2wCOcIvuAFcyq0Mwe6+70ezoZ5WfiaIAzzwWfq72jyaLeg8TViGNSmw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime-corejs3": "^7.22.15",
|
"@babel/runtime-corejs3": "^7.22.15",
|
||||||
|
"@scarf/scarf": "=1.4.0",
|
||||||
"@swagger-api/apidom-core": ">=1.0.0-alpha.9 <1.0.0-beta.0",
|
"@swagger-api/apidom-core": ">=1.0.0-alpha.9 <1.0.0-beta.0",
|
||||||
"@swagger-api/apidom-error": ">=1.0.0-alpha.9 <1.0.0-beta.0",
|
"@swagger-api/apidom-error": ">=1.0.0-alpha.9 <1.0.0-beta.0",
|
||||||
"@swagger-api/apidom-json-pointer": ">=1.0.0-alpha.9 <1.0.0-beta.0",
|
"@swagger-api/apidom-json-pointer": ">=1.0.0-alpha.9 <1.0.0-beta.0",
|
||||||
@ -20646,14 +20653,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.8.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.0.tgz",
|
||||||
"integrity": "sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==",
|
"integrity": "sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.8.1",
|
"@typescript-eslint/eslint-plugin": "8.8.0",
|
||||||
"@typescript-eslint/parser": "8.8.1",
|
"@typescript-eslint/parser": "8.8.0",
|
||||||
"@typescript-eslint/utils": "8.8.1"
|
"@typescript-eslint/utils": "8.8.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@ -20911,17 +20918,17 @@
|
|||||||
"integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg=="
|
"integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg=="
|
||||||
},
|
},
|
||||||
"node_modules/untyped": {
|
"node_modules/untyped": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.0.tgz",
|
||||||
"integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==",
|
"integrity": "sha512-o2Vjmn2dal08BzCcINxSmWuAteReUUiXseii5VRhmxyLF0b21K0iKZQ9fMYK7RWspVkY+0saqaVQNq4roe3Efg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.25.7",
|
"@babel/core": "^7.25.2",
|
||||||
"@babel/standalone": "^7.25.7",
|
"@babel/standalone": "^7.25.6",
|
||||||
"@babel/types": "^7.25.7",
|
"@babel/types": "^7.25.6",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
"jiti": "^2.3.1",
|
"jiti": "^2.0.0",
|
||||||
"mri": "^1.2.0",
|
"mri": "^1.2.0",
|
||||||
"scule": "^1.3.0"
|
"scule": "^1.3.0"
|
||||||
},
|
},
|
||||||
@ -20945,9 +20952,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/untyped/node_modules/jiti": {
|
"node_modules/untyped/node_modules/jiti": {
|
||||||
"version": "2.3.3",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.2.1.tgz",
|
||||||
"integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==",
|
"integrity": "sha512-weIl/Bv3G0J3UKamLxEA2G+FfQ33Z1ZkQJGPjKFV21zQdKWu2Pi6o4elpj2uEl5XdFJZ9xzn1fsanWTFSt45zw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -21881,9 +21888,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webdriver/node_modules/@types/node": {
|
"node_modules/webdriver/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@ -21936,9 +21943,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webdriverio/node_modules/@types/node": {
|
"node_modules/webdriverio/node_modules/@types/node": {
|
||||||
"version": "20.16.11",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
|
@ -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.0-1730331602",
|
"@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",
|
||||||
@ -328,12 +328,18 @@
|
|||||||
},
|
},
|
||||||
"test:e2e:watch": {
|
"test:e2e:watch": {
|
||||||
"command": "wdio run ./tests/wdio.conf.ts",
|
"command": "wdio run ./tests/wdio.conf.ts",
|
||||||
|
"dependencies": [
|
||||||
|
"build"
|
||||||
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
|
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test:watch": {
|
"test:watch": {
|
||||||
"command": "wdio run ./wdio.conf.ts",
|
"command": "wdio run ./wdio.conf.ts",
|
||||||
|
"dependencies": [
|
||||||
|
"build"
|
||||||
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"TS_NODE_PROJECT": "tsconfig.test.json"
|
"TS_NODE_PROJECT": "tsconfig.test.json"
|
||||||
}
|
}
|
||||||
|
100
web/src/admin/admin-settings/AdminSettingsFooterLinks.ts
Normal file
100
web/src/admin/admin-settings/AdminSettingsFooterLinks.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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 { FooterLink } from "@goauthentik/api";
|
||||||
|
|
||||||
|
export interface IFooterLinkInput {
|
||||||
|
footerLink: FooterLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEGAL_SCHEMES = ["http://", "https://", "mailto:"];
|
||||||
|
const hasLegalScheme = (url: string) =>
|
||||||
|
LEGAL_SCHEMES.some((scheme) => url.substr(0, scheme.length).toLowerCase() === scheme);
|
||||||
|
|
||||||
|
@customElement("ak-admin-settings-footer-link")
|
||||||
|
export class FooterLinkInput extends AkControlElement<FooterLink> {
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFInputGroup,
|
||||||
|
PFFormControl,
|
||||||
|
css`
|
||||||
|
.pf-c-input-group input#linkname {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: Object, attribute: false })
|
||||||
|
footerLink: FooterLink = {
|
||||||
|
name: "",
|
||||||
|
href: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
@queryAll(".ak-form-control")
|
||||||
|
controls?: HTMLInputElement[];
|
||||||
|
|
||||||
|
json() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
|
||||||
|
) as unknown as FooterLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isValid() {
|
||||||
|
const href = this.json()?.href ?? "";
|
||||||
|
return hasLegalScheme(href) && URL.canParse(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const onChange = () => {
|
||||||
|
this.dispatchEvent(new Event("change", { composed: true, bubbles: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return html` <div class="pf-c-input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
@change=${onChange}
|
||||||
|
value=${this.footerLink.name}
|
||||||
|
id="linkname"
|
||||||
|
class="pf-c-form-control ak-form-control"
|
||||||
|
name="name"
|
||||||
|
placeholder=${msg("Link Title")}
|
||||||
|
tabindex="1"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
@change=${onChange}
|
||||||
|
value="${ifDefined(this.footerLink.href ?? undefined)}"
|
||||||
|
class="pf-c-form-control ak-form-control"
|
||||||
|
required
|
||||||
|
placeholder=${msg("URL")}
|
||||||
|
name="href"
|
||||||
|
tabindex="1"
|
||||||
|
/>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function akFooterLinkInput(properties: IFooterLinkInput) {
|
||||||
|
return html`<ak-admin-settings-footer-link
|
||||||
|
${spread(properties as unknown as Spread)}
|
||||||
|
></ak-admin-settings-footer-link>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-admin-settings-footer-link": FooterLinkInput;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user