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]
|
||||
current_version = 2024.10.0
|
||||
current_version = 2024.10.4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2024.10.0"
|
||||
__version__ = "2024.10.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ API Browser - {{ brand.branding_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% 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: dark)">
|
||||
{% endblock %}
|
||||
|
@ -27,7 +27,8 @@ def blueprint_tester(file_name: Path) -> Callable:
|
||||
base = Path("blueprints/")
|
||||
rel_path = Path(file_name).relative_to(base)
|
||||
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())
|
||||
|
||||
return tester
|
||||
|
@ -4,7 +4,7 @@ from collections.abc import Callable
|
||||
|
||||
from django.http.request import HttpRequest
|
||||
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
|
||||
|
||||
@ -18,10 +18,12 @@ class BrandMiddleware:
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
locale_to_set = None
|
||||
if not hasattr(request, "brand"):
|
||||
brand = get_brand_for_request(request)
|
||||
request.brand = brand
|
||||
locale = brand.default_locale
|
||||
if locale != "":
|
||||
activate(locale)
|
||||
return self.get_response(request)
|
||||
locale_to_set = locale
|
||||
with override(locale_to_set):
|
||||
return self.get_response(request)
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Authenticator Devices API Views"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework.fields import (
|
||||
@ -40,7 +41,11 @@ class DeviceSerializer(MetaNameSerializer):
|
||||
def get_extra_description(self, instance: Device) -> str:
|
||||
"""Get extra description"""
|
||||
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):
|
||||
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
||||
return ""
|
||||
|
@ -5,7 +5,7 @@ from contextvars import ContextVar
|
||||
from uuid import uuid4
|
||||
|
||||
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 structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
||||
|
||||
@ -31,17 +31,19 @@ class ImpersonateMiddleware:
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
# No permission checks are done here, they need to be checked before
|
||||
# SESSION_KEY_IMPERSONATE_USER is set.
|
||||
locale_to_set = None
|
||||
if request.user.is_authenticated:
|
||||
locale = request.user.locale(request)
|
||||
if locale != "":
|
||||
activate(locale)
|
||||
locale_to_set = locale
|
||||
|
||||
if SESSION_KEY_IMPERSONATE_USER in request.session:
|
||||
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||
# Ensure that the user is active, otherwise nothing will work
|
||||
request.user.is_active = True
|
||||
|
||||
return self.get_response(request)
|
||||
with override(locale_to_set):
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class RequestIDMiddleware:
|
||||
|
@ -129,6 +129,11 @@ class SourceFlowManager:
|
||||
)
|
||||
new_connection.user = self.request.user
|
||||
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
|
||||
|
||||
action, connection = self.matcher.get_user_action(self.identifier, self.user_properties)
|
||||
|
@ -15,8 +15,8 @@
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
||||
{% versioned_script "dist/poly-%v.js" %}
|
||||
{% versioned_script "dist/standalone/loading/index-%v.js" %}
|
||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% load authentik_core %}
|
||||
|
||||
{% 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="#ffffff" media="(prefers-color-scheme: light)">
|
||||
{% include "base/header_js.html" %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% load authentik_core %}
|
||||
|
||||
{% 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: dark)">
|
||||
{% include "base/header_js.html" %}
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from django import template
|
||||
from django.templatetags.static import static as static_loader
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from authentik import get_full_version
|
||||
|
||||
@ -12,10 +11,4 @@ register = template.Library()
|
||||
@register.simple_tag()
|
||||
def versioned_script(path: str) -> str:
|
||||
"""Wrapper around {% static %} tag that supports setting the version"""
|
||||
returned_lines = [
|
||||
(
|
||||
f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
|
||||
'" type="module"></script>'
|
||||
),
|
||||
]
|
||||
return mark_safe("".join(returned_lines)) # nosec
|
||||
return static_loader(path.replace("%v", get_full_version()))
|
||||
|
@ -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.policies.dummy.models import DummyPolicy
|
||||
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.saml.models import SAMLProvider
|
||||
|
||||
@ -24,7 +24,7 @@ class TestApplicationsAPI(APITestCase):
|
||||
self.user = create_test_admin_user()
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
redirect_uris="http://some-other-domain",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://some-other-domain")],
|
||||
authorization_flow=create_test_flow(),
|
||||
)
|
||||
self.allowed: Application = Application.objects.create(
|
||||
|
@ -81,6 +81,22 @@ class TestSourceFlowManager(TestCase):
|
||||
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):
|
||||
"""Test un-authenticated user linking"""
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
|
@ -31,6 +31,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
||||
"name": uid,
|
||||
"authorization_flow": str(create_test_flow().pk),
|
||||
"invalidation_flow": str(create_test_flow().pk),
|
||||
"redirect_uris": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -57,6 +58,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
||||
"name": uid,
|
||||
"authorization_flow": "",
|
||||
"invalidation_flow": "",
|
||||
"redirect_uris": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -24,6 +24,7 @@ from rest_framework.fields import (
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.validators import UniqueValidator
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -181,7 +182,10 @@ class CertificateDataSerializer(PassiveSerializer):
|
||||
class CertificateGenerationSerializer(PassiveSerializer):
|
||||
"""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"))
|
||||
validity_days = IntegerField(initial=365)
|
||||
alg = ChoiceField(default=PrivateKeyAlg.RSA, choices=PrivateKeyAlg.choices)
|
||||
@ -242,11 +246,10 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
def generate(self, request: Request) -> Response:
|
||||
"""Generate a new, self-signed certificate-key pair"""
|
||||
data = CertificateGenerationSerializer(data=request.data)
|
||||
if not data.is_valid():
|
||||
return Response(data.errors, status=400)
|
||||
data.is_valid(raise_exception=True)
|
||||
raw_san = data.validated_data.get("subject_alt_name", "")
|
||||
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.build(
|
||||
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.lib.config import CONFIG
|
||||
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):
|
||||
@ -89,6 +89,17 @@ class TestCrypto(APITestCase):
|
||||
self.assertIsInstance(ext[1], DNSName)
|
||||
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):
|
||||
"""Test Builder (via API)"""
|
||||
self.client.force_login(create_test_admin_user())
|
||||
@ -263,7 +274,7 @@ class TestCrypto(APITestCase):
|
||||
client_id="test",
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
signing_key=keypair,
|
||||
)
|
||||
response = self.client.get(
|
||||
@ -295,7 +306,7 @@ class TestCrypto(APITestCase):
|
||||
client_id="test",
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
signing_key=keypair,
|
||||
)
|
||||
response = self.client.get(
|
||||
|
@ -16,13 +16,28 @@ class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
|
||||
|
||||
class Meta:
|
||||
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",
|
||||
"outpost_set",
|
||||
"connection_expiry",
|
||||
"delete_token_on_disconnect",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
extra_kwargs = {
|
||||
"authorization_flow": {"required": True, "allow_null": False},
|
||||
}
|
||||
|
||||
|
||||
class RACProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% load authentik_core %}
|
||||
|
||||
{% 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="#ffffff" media="(prefers-color-scheme: light)">
|
||||
<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,
|
||||
"authentication_flow": None,
|
||||
"authorization_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"property_mappings": [],
|
||||
"connection_expiry": "hours=8",
|
||||
"delete_token_on_disconnect": False,
|
||||
@ -121,7 +120,6 @@ class TestEndpointsAPI(APITestCase):
|
||||
"name": self.provider.name,
|
||||
"authentication_flow": None,
|
||||
"authorization_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"property_mappings": [],
|
||||
"component": "ak-provider-rac-form",
|
||||
"assigned_application_slug": self.app.slug,
|
||||
@ -151,7 +149,6 @@ class TestEndpointsAPI(APITestCase):
|
||||
"name": self.provider.name,
|
||||
"authentication_flow": None,
|
||||
"authorization_flow": None,
|
||||
"invalidation_flow": None,
|
||||
"property_mappings": [],
|
||||
"component": "ak-provider-rac-form",
|
||||
"assigned_application_slug": self.app.slug,
|
||||
|
@ -18,7 +18,7 @@ window.authentik.flow = {
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{% versioned_script "dist/flow/FlowInterface-%v.js" %}
|
||||
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||
<style>
|
||||
:root {
|
||||
--ak-flow-background: url("{{ flow.background_url }}");
|
||||
|
@ -89,6 +89,10 @@ class PasswordPolicy(Policy):
|
||||
|
||||
def passes_static(self, password: str, request: PolicyRequest) -> PolicyResult:
|
||||
"""Check static rules"""
|
||||
error_message = self.error_message
|
||||
if error_message == "":
|
||||
error_message = _("Invalid password.")
|
||||
|
||||
if len(password) < self.length_min:
|
||||
LOGGER.debug("password failed", check="static", reason="length")
|
||||
return PolicyResult(False, self.error_message)
|
||||
|
@ -159,7 +159,10 @@ class LDAPOutpostConfigViewSet(ListModelMixin, GenericViewSet):
|
||||
access_response = PolicyResult(result.passing)
|
||||
response = self.LDAPCheckAccessSerializer(
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
@ -1,15 +1,18 @@
|
||||
"""OAuth2Provider API Views"""
|
||||
|
||||
from copy import copy
|
||||
from re import compile
|
||||
from re import error as RegexError
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
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.request import Request
|
||||
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.models import Provider
|
||||
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
|
||||
|
||||
|
||||
class RedirectURISerializer(PassiveSerializer):
|
||||
"""A single allowed redirect URI entry"""
|
||||
|
||||
matching_mode = ChoiceField(choices=RedirectURIMatchingMode.choices)
|
||||
url = CharField()
|
||||
|
||||
|
||||
class OAuth2ProviderSerializer(ProviderSerializer):
|
||||
"""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:
|
||||
model = OAuth2Provider
|
||||
fields = ProviderSerializer.Meta.fields + [
|
||||
@ -79,7 +108,6 @@ class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"refresh_token_validity",
|
||||
"include_claims_in_id_token",
|
||||
"signing_key",
|
||||
"redirect_uris",
|
||||
"sub_mode",
|
||||
"property_mappings",
|
||||
"issuer_mode",
|
||||
|
@ -7,7 +7,7 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
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):
|
||||
@ -46,9 +46,9 @@ class RedirectUriError(OAuth2Error):
|
||||
)
|
||||
|
||||
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__()
|
||||
self.provided_uri = provided_uri
|
||||
self.allowed_uris = allowed_uris
|
||||
|
@ -11,13 +11,16 @@ class Migration(migrations.Migration):
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="accesstoken",
|
||||
index=models.Index(fields=["token"], name="authentik_p_token_4bc870_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="refreshtoken",
|
||||
index=models.Index(fields=["token"], name="authentik_p_token_1a841f_idx"),
|
||||
),
|
||||
]
|
||||
# Original preserved
|
||||
# See https://github.com/goauthentik/authentik/issues/11874
|
||||
# operations = [
|
||||
# migrations.AddIndex(
|
||||
# model_name="accesstoken",
|
||||
# index=models.Index(fields=["token"], name="authentik_p_token_4bc870_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),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name="accesstoken",
|
||||
name="authentik_p_token_4bc870_idx",
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name="refreshtoken",
|
||||
name="authentik_p_token_1a841f_idx",
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="accesstoken",
|
||||
index=models.Index(fields=["token", "provider"], name="authentik_p_token_f99422_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="refreshtoken",
|
||||
index=models.Index(fields=["token", "provider"], name="authentik_p_token_a1d921_idx"),
|
||||
),
|
||||
]
|
||||
# Original preserved
|
||||
# See https://github.com/goauthentik/authentik/issues/11874
|
||||
# operations = [
|
||||
# migrations.RemoveIndex(
|
||||
# model_name="accesstoken",
|
||||
# name="authentik_p_token_4bc870_idx",
|
||||
# ),
|
||||
# migrations.RemoveIndex(
|
||||
# model_name="refreshtoken",
|
||||
# name="authentik_p_token_1a841f_idx",
|
||||
# ),
|
||||
# migrations.AddIndex(
|
||||
# model_name="accesstoken",
|
||||
# index=models.Index(fields=["token", "provider"], name="authentik_p_token_f99422_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):
|
||||
|
||||
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"),
|
||||
]
|
||||
|
||||
|
@ -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 binascii
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
from dataclasses import asdict, dataclass
|
||||
from functools import cached_property
|
||||
from hashlib import sha256
|
||||
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.rsa import RSAPrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||
from dacite import Config
|
||||
from dacite.core import from_dict
|
||||
from django.contrib.postgres.indexes import HashIndex
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
@ -76,11 +78,25 @@ class IssuerMode(models.TextChoices):
|
||||
"""Configure how the `iss` field is created."""
|
||||
|
||||
GLOBAL = "global", _("Same identifier is used for all providers")
|
||||
PER_PROVIDER = "per_provider", _(
|
||||
"Each provider has a different issuer, based on the application slug."
|
||||
PER_PROVIDER = (
|
||||
"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):
|
||||
"""Response Type required by the client."""
|
||||
|
||||
@ -155,11 +171,9 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
||||
verbose_name=_("Client Secret"),
|
||||
default=generate_client_secret,
|
||||
)
|
||||
redirect_uris = models.TextField(
|
||||
default="",
|
||||
blank=True,
|
||||
_redirect_uris = models.JSONField(
|
||||
default=dict,
|
||||
verbose_name=_("Redirect URIs"),
|
||||
help_text=_("Enter each URI on a new line."),
|
||||
)
|
||||
|
||||
include_claims_in_id_token = models.BooleanField(
|
||||
@ -270,12 +284,33 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
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
|
||||
def launch_url(self) -> str | None:
|
||||
"""Guess launch_url based on first redirect_uri"""
|
||||
if self.redirect_uris == "":
|
||||
redirects = self.redirect_uris
|
||||
if len(redirects) < 1:
|
||||
return None
|
||||
main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
|
||||
main_url = redirects[0].url
|
||||
try:
|
||||
launch_url = urlparse(main_url)._replace(path="")
|
||||
return urlunparse(launch_url)
|
||||
@ -418,7 +453,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["token", "provider"]),
|
||||
HashIndex(fields=["token"]),
|
||||
]
|
||||
verbose_name = _("OAuth2 Access Token")
|
||||
verbose_name_plural = _("OAuth2 Access Tokens")
|
||||
@ -464,7 +499,7 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["token", "provider"]),
|
||||
HashIndex(fields=["token"]),
|
||||
]
|
||||
verbose_name = _("OAuth2 Refresh Token")
|
||||
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.core.models import Application
|
||||
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):
|
||||
@ -21,7 +27,7 @@ class TestAPI(APITestCase):
|
||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
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.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")
|
||||
def test_launch_url(self):
|
||||
"""Test launch_url"""
|
||||
self.provider.redirect_uris = (
|
||||
"https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/\n"
|
||||
)
|
||||
self.provider.redirect_uris = [
|
||||
RedirectURI(
|
||||
RedirectURIMatchingMode.REGEX,
|
||||
"https://[\\d\\w]+.pr.test.goauthentik.io/source/oauth/callback/authentik/",
|
||||
),
|
||||
]
|
||||
self.provider.save()
|
||||
self.provider.refresh_from_db()
|
||||
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,
|
||||
GrantTypes,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
ScopeMapping,
|
||||
)
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
@ -39,7 +41,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid/Foo",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
request = self.factory.get(
|
||||
@ -64,7 +66,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid/Foo",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
request = self.factory.get(
|
||||
@ -84,7 +86,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
@ -106,7 +108,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="data:local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "data:local.invalid")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get(
|
||||
@ -125,7 +127,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="",
|
||||
redirect_uris=[],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
@ -140,7 +142,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
provider.refresh_from_db()
|
||||
self.assertEqual(provider.redirect_uris, "+")
|
||||
self.assertEqual(provider.redirect_uris, [RedirectURI(RedirectURIMatchingMode.STRICT, "+")])
|
||||
|
||||
def test_invalid_redirect_uri_regex(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
@ -148,7 +150,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid?",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid?")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
@ -170,7 +172,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="+",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "+")],
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
@ -213,7 +215,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid/Foo",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")],
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
@ -301,7 +303,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
@ -343,7 +345,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -420,7 +422,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
signing_key=self.keypair,
|
||||
encryption_key=self.keypair,
|
||||
)
|
||||
@ -486,7 +488,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
@ -541,7 +543,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id=generate_id(),
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -599,7 +601,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id=generate_id(),
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
|
||||
|
@ -3,6 +3,7 @@
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from authentik.core.models import Application, Group
|
||||
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.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"""
|
||||
res = self.client.get(reverse("authentik_providers_oauth2_root:device-login"))
|
||||
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):
|
||||
"""Test no flow"""
|
||||
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.lib.generators import generate_id
|
||||
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
|
||||
|
||||
|
||||
@ -23,7 +30,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
self.app = Application.objects.create(
|
||||
@ -118,7 +125,7 @@ class TesOAuth2Introspection(OAuthTestCase):
|
||||
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
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.models import CertificateKeyPair
|
||||
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
|
||||
|
||||
TEST_CORDS_CERT = """
|
||||
@ -49,7 +49,7 @@ class TestJWKS(OAuthTestCase):
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||
@ -68,7 +68,7 @@ class TestJWKS(OAuthTestCase):
|
||||
name="test",
|
||||
client_id="test",
|
||||
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)
|
||||
response = self.client.get(
|
||||
@ -82,7 +82,7 @@ class TestJWKS(OAuthTestCase):
|
||||
name="test",
|
||||
client_id="test",
|
||||
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),
|
||||
)
|
||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||
@ -99,7 +99,7 @@ class TestJWKS(OAuthTestCase):
|
||||
name="test",
|
||||
client_id="test",
|
||||
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),
|
||||
encryption_key=create_test_cert(PrivateKeyAlg.ECDSA),
|
||||
)
|
||||
@ -122,7 +122,7 @@ class TestJWKS(OAuthTestCase):
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
signing_key=cert,
|
||||
)
|
||||
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.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
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
|
||||
|
||||
|
||||
@ -22,7 +29,7 @@ class TesOAuth2Revoke(OAuthTestCase):
|
||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
self.app = Application.objects.create(
|
||||
|
@ -22,6 +22,8 @@ from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
AuthorizationCode,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
RefreshToken,
|
||||
ScopeMapping,
|
||||
)
|
||||
@ -42,7 +44,7 @@ class TestToken(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://TestServer",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://TestServer")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
@ -69,7 +71,7 @@ class TestToken(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
@ -90,7 +92,7 @@ class TestToken(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
@ -118,7 +120,7 @@ class TestToken(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
@ -157,7 +159,7 @@ class TestToken(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
signing_key=self.keypair,
|
||||
encryption_key=self.keypair,
|
||||
)
|
||||
@ -188,7 +190,7 @@ class TestToken(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -250,7 +252,7 @@ class TestToken(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -308,7 +310,7 @@ class TestToken(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
|
@ -19,7 +19,12 @@ from authentik.providers.oauth2.constants import (
|
||||
SCOPE_OPENID_PROFILE,
|
||||
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.views.jwks import JWKSView
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
@ -54,7 +59,7 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
|
||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||
signing_key=self.cert,
|
||||
)
|
||||
self.provider.jwks_sources.add(self.source)
|
||||
|
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
||||
TOKEN_TYPE,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@ -33,7 +39,7 @@ class TestTokenClientCredentialsStandard(OAuthTestCase):
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||
@ -107,6 +113,48 @@ class TestTokenClientCredentialsStandard(OAuthTestCase):
|
||||
{"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):
|
||||
"""test successful"""
|
||||
response = self.client.post(
|
||||
|
@ -20,7 +20,12 @@ from authentik.providers.oauth2.constants import (
|
||||
TOKEN_TYPE,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@ -34,7 +39,7 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||
|
@ -19,7 +19,12 @@ from authentik.providers.oauth2.constants import (
|
||||
TOKEN_TYPE,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@ -33,7 +38,7 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
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.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.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE
|
||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping
|
||||
from authentik.providers.oauth2.constants import (
|
||||
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
|
||||
|
||||
|
||||
@ -24,7 +35,7 @@ class TestTokenDeviceCode(OAuthTestCase):
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||
@ -80,3 +91,28 @@ class TestTokenDeviceCode(OAuthTestCase):
|
||||
},
|
||||
)
|
||||
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.lib.generators import generate_id
|
||||
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
|
||||
|
||||
|
||||
@ -30,7 +35,7 @@ class TestTokenPKCE(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
@ -93,7 +98,7 @@ class TestTokenPKCE(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
@ -154,7 +159,7 @@ class TestTokenPKCE(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
@ -210,7 +215,7 @@ class TestTokenPKCE(OAuthTestCase):
|
||||
name=generate_id(),
|
||||
client_id="test",
|
||||
authorization_flow=flow,
|
||||
redirect_uris="foo://localhost",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
|
||||
access_code_validity="seconds=100",
|
||||
)
|
||||
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.events.models import Event, EventAction
|
||||
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
|
||||
|
||||
|
||||
@ -25,7 +32,7 @@ class TestUserinfo(OAuthTestCase):
|
||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
|
||||
signing_key=create_test_cert(),
|
||||
)
|
||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||
|
@ -56,6 +56,8 @@ from authentik.providers.oauth2.models import (
|
||||
AuthorizationCode,
|
||||
GrantTypes,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
ResponseMode,
|
||||
ResponseTypes,
|
||||
ScopeMapping,
|
||||
@ -187,40 +189,39 @@ class OAuthAuthorizationParams:
|
||||
|
||||
def check_redirect_uri(self):
|
||||
"""Redirect URI validation."""
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
allowed_redirect_urls = self.provider.redirect_uris
|
||||
if not self.redirect_uri:
|
||||
LOGGER.warning("Missing redirect uri.")
|
||||
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)
|
||||
self.provider.redirect_uris = self.redirect_uri
|
||||
self.provider.redirect_uris = [
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, self.redirect_uri)
|
||||
]
|
||||
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()
|
||||
|
||||
try:
|
||||
if not any(fullmatch(x, self.redirect_uri) for x in allowed_redirect_urls):
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri (regex comparison)",
|
||||
redirect_uri_given=self.redirect_uri,
|
||||
redirect_uri_expected=allowed_redirect_urls,
|
||||
)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
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_given=self.redirect_uri,
|
||||
redirect_uri_expected=allowed_redirect_urls,
|
||||
)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls) from None
|
||||
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,
|
||||
)
|
||||
if not match_found:
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
# Check against forbidden schemes
|
||||
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
|
@ -5,7 +5,7 @@ from typing import Any
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
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 authentik.brands.models import Brand
|
||||
@ -47,6 +47,9 @@ class CodeValidatorView(PolicyAccessView):
|
||||
self.provider = self.token.provider
|
||||
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):
|
||||
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
|
||||
planner = FlowPlanner(self.provider.authorization_flow)
|
||||
@ -122,7 +125,7 @@ class OAuthDeviceCodeChallenge(Challenge):
|
||||
class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
|
||||
"""Response that includes the user-entered device code"""
|
||||
|
||||
code = IntegerField()
|
||||
code = CharField()
|
||||
component = CharField(default="ak-provider-oauth2-device-code")
|
||||
|
||||
def validate_code(self, code: int) -> HttpResponse | None:
|
||||
|
@ -162,5 +162,5 @@ class ProviderInfoView(View):
|
||||
OAuth2Provider, pk=application.provider_id
|
||||
)
|
||||
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
|
||||
|
@ -58,7 +58,9 @@ from authentik.providers.oauth2.models import (
|
||||
ClientTypes,
|
||||
DeviceToken,
|
||||
OAuth2Provider,
|
||||
RedirectURIMatchingMode,
|
||||
RefreshToken,
|
||||
ScopeMapping,
|
||||
)
|
||||
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
|
||||
from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES
|
||||
@ -77,7 +79,7 @@ class TokenParams:
|
||||
redirect_uri: str
|
||||
grant_type: str
|
||||
state: str
|
||||
scope: list[str]
|
||||
scope: set[str]
|
||||
|
||||
provider: OAuth2Provider
|
||||
|
||||
@ -112,11 +114,26 @@ class TokenParams:
|
||||
redirect_uri=request.POST.get("redirect_uri", ""),
|
||||
grant_type=request.POST.get("grant_type", ""),
|
||||
state=request.POST.get("state", ""),
|
||||
scope=request.POST.get("scope", "").split(),
|
||||
scope=set(request.POST.get("scope", "").split()),
|
||||
# PKCE parameter.
|
||||
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):
|
||||
with start_span(
|
||||
op="authentik.providers.oauth2.token.policy",
|
||||
@ -149,7 +166,7 @@ class TokenParams:
|
||||
client_id=self.provider.client_id,
|
||||
)
|
||||
raise TokenError("invalid_client")
|
||||
|
||||
self.__check_scopes()
|
||||
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||
with start_span(
|
||||
op="authentik.providers.oauth2.post.parse.code",
|
||||
@ -179,42 +196,7 @@ class TokenParams:
|
||||
LOGGER.warning("Missing authorization code")
|
||||
raise TokenError("invalid_grant")
|
||||
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
# 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.__check_redirect_uri(request)
|
||||
|
||||
self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
|
||||
if not self.authorization_code:
|
||||
@ -254,6 +236,48 @@ class TokenParams:
|
||||
if not self.authorization_code.code_challenge and self.code_verifier:
|
||||
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):
|
||||
if not raw_token:
|
||||
LOGGER.warning("Missing refresh token")
|
||||
@ -497,7 +521,7 @@ class TokenView(View):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
allowed_origins = []
|
||||
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)
|
||||
return response
|
||||
|
||||
@ -710,7 +734,7 @@ class TokenView(View):
|
||||
"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 = RefreshToken(
|
||||
user=self.params.device_code.user,
|
||||
|
@ -108,7 +108,7 @@ class UserInfoView(View):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
allowed_origins = []
|
||||
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)
|
||||
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.utils import ModelSerializer, PassiveSerializer
|
||||
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.views.provider import ProviderInfoView
|
||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||
@ -39,7 +40,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
||||
"""ProxyProvider Serializer"""
|
||||
|
||||
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")
|
||||
|
||||
def validate_basic_auth_enabled(self, value: bool) -> bool:
|
||||
@ -121,7 +122,6 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"basic_auth_password_attribute": ["iexact"],
|
||||
"basic_auth_user_attribute": ["iexact"],
|
||||
"mode": ["iexact"],
|
||||
"redirect_uris": ["iexact"],
|
||||
"cookie_domain": ["iexact"],
|
||||
}
|
||||
search_fields = ["name"]
|
||||
|
@ -13,7 +13,13 @@ from rest_framework.serializers import Serializer
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.models import DomainlessURLValidator
|
||||
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"
|
||||
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))
|
||||
|
||||
|
||||
def _get_callback_url(uri: str) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
urljoin(uri, "outpost.goauthentik.io/callback")
|
||||
+ f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
||||
uri + f"\\?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
||||
]
|
||||
)
|
||||
def _get_callback_url(uri: str) -> list[RedirectURI]:
|
||||
return [
|
||||
RedirectURI(
|
||||
RedirectURIMatchingMode.STRICT,
|
||||
urljoin(uri, "outpost.goauthentik.io/callback") + f"?{OUTPOST_CALLBACK_SIGNATURE}=true",
|
||||
),
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, uri + f"?{OUTPOST_CALLBACK_SIGNATURE}=true"),
|
||||
]
|
||||
|
||||
|
||||
class ProxyMode(models.TextChoices):
|
||||
|
@ -19,6 +19,7 @@ SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
|
||||
class User(BaseUser):
|
||||
"""Modified User schema with added externalId field"""
|
||||
|
||||
id: str | int | None = None
|
||||
schemas: list[str] = [SCIM_USER_SCHEMA]
|
||||
externalId: str | None = None
|
||||
meta: dict | None = None
|
||||
@ -27,6 +28,7 @@ class User(BaseUser):
|
||||
class Group(BaseGroup):
|
||||
"""Modified Group schema with added externalId field"""
|
||||
|
||||
id: str | int | None = None
|
||||
schemas: list[str] = [SCIM_GROUP_SCHEMA]
|
||||
externalId: str | None = None
|
||||
meta: dict | None = None
|
||||
|
@ -53,7 +53,7 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
|
||||
except LookupError:
|
||||
return None
|
||||
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:
|
||||
return None
|
||||
return str(obj)
|
||||
|
@ -53,7 +53,7 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
|
||||
except LookupError:
|
||||
return None
|
||||
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:
|
||||
return None
|
||||
return str(obj)
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""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.db import connections
|
||||
@ -16,22 +18,21 @@ monitoring_set = Signal()
|
||||
|
||||
|
||||
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:
|
||||
"""Check for HTTP-Basic auth"""
|
||||
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
|
||||
auth_type, _, given_credentials = auth_header.partition(" ")
|
||||
credentials = f"monitor:{settings.SECRET_KEY}"
|
||||
expected = b64encode(str.encode(credentials)).decode()
|
||||
authed = auth_type == "Basic" and given_credentials == expected
|
||||
authed = auth_type == "Bearer" and compare_digest(given_credentials, self.monitoring_key)
|
||||
if not authed and not settings.DEBUG:
|
||||
response = HttpResponse(status=401)
|
||||
response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"'
|
||||
return response
|
||||
|
||||
return HttpResponse(status=401)
|
||||
monitoring_set.send_robust(self)
|
||||
|
||||
return ExportToDjangoView(request)
|
||||
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""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.urls import reverse
|
||||
|
||||
@ -10,6 +11,16 @@ from django.urls import reverse
|
||||
class TestRoot(TestCase):
|
||||
"""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):
|
||||
"""Test monitoring without any credentials"""
|
||||
response = self.client.get(reverse("metrics"))
|
||||
@ -17,8 +28,7 @@ class TestRoot(TestCase):
|
||||
|
||||
def test_monitoring_ok(self):
|
||||
"""Test monitoring with credentials"""
|
||||
creds = "Basic " + b64encode(f"monitor:{settings.SECRET_KEY}".encode()).decode("utf-8")
|
||||
auth_headers = {"HTTP_AUTHORIZATION": creds}
|
||||
auth_headers = {"HTTP_AUTHORIZATION": f"Bearer {self.token}"}
|
||||
response = self.client.get(reverse("metrics"), **auth_headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -17,6 +17,7 @@ class CaptchaStageSerializer(StageSerializer):
|
||||
"private_key",
|
||||
"js_url",
|
||||
"api_url",
|
||||
"interactive",
|
||||
"score_min_threshold",
|
||||
"score_max_threshold",
|
||||
"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):
|
||||
"""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."))
|
||||
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_max_threshold = models.FloatField(default=1.0) # Default values for reCaptcha
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
from django.http.response import HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from requests import RequestException
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.fields import BooleanField, CharField
|
||||
from rest_framework.serializers import ValidationError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -24,10 +24,12 @@ PLAN_CONTEXT_CAPTCHA = "captcha"
|
||||
class CaptchaChallenge(WithUserInfoChallenge):
|
||||
"""Site public key"""
|
||||
|
||||
site_key = CharField()
|
||||
js_url = CharField()
|
||||
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):
|
||||
"""Validate captcha token"""
|
||||
@ -103,6 +105,7 @@ class CaptchaStageView(ChallengeStageView):
|
||||
data={
|
||||
"js_url": self.executor.current_stage.js_url,
|
||||
"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,
|
||||
"site_key": current_stage.captcha_stage.public_key,
|
||||
"interactive": current_stage.captcha_stage.interactive,
|
||||
}
|
||||
if current_stage.captcha_stage
|
||||
else None
|
||||
|
@ -21,7 +21,7 @@ from authentik.flows.challenge import (
|
||||
WithUserInfoChallenge,
|
||||
)
|
||||
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.stage import ChallengeStageView
|
||||
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,
|
||||
}
|
||||
)
|
||||
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
|
||||
if recovery_flow.exists():
|
||||
recovery_flow: Flow | None = self.request.brand.flow_recovery
|
||||
if recovery_flow:
|
||||
recover_url = reverse(
|
||||
"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)
|
||||
return challenge
|
||||
|
@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
||||
from django.core.exceptions import PermissionDenied
|
||||
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.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
@ -57,6 +57,9 @@ class TestPasswordStage(FlowTestCase):
|
||||
def test_recovery_flow_link(self):
|
||||
"""Test link to the default recovery flow"""
|
||||
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()])
|
||||
session = self.client.session
|
||||
|
@ -101,7 +101,6 @@ entries:
|
||||
- !KeyOf prompt-field-email
|
||||
- !KeyOf prompt-field-password
|
||||
- !KeyOf prompt-field-password-repeat
|
||||
validation_policies: []
|
||||
id: stage-default-oobe-password
|
||||
identifiers:
|
||||
name: stage-default-oobe-password
|
||||
|
@ -2,6 +2,17 @@ version: 1
|
||||
metadata:
|
||||
name: Default - Password change flow
|
||||
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:
|
||||
designation: stage_configuration
|
||||
name: Change Password
|
||||
@ -39,6 +50,8 @@ entries:
|
||||
fields:
|
||||
- !KeyOf prompt-field-password
|
||||
- !KeyOf prompt-field-password-repeat
|
||||
validation_policies:
|
||||
- !KeyOf default-password-change-password-policy
|
||||
identifiers:
|
||||
name: default-password-change-prompt
|
||||
id: default-password-change-prompt
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2024.10.0 Blueprint schema",
|
||||
"title": "authentik 2024.10.4 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -5570,9 +5570,30 @@
|
||||
"description": "Key used to encrypt the tokens. When set, tokens will be encrypted and returned as JWEs."
|
||||
},
|
||||
"redirect_uris": {
|
||||
"type": "string",
|
||||
"title": "Redirect URIs",
|
||||
"description": "Enter each URI on a new line."
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matching_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"strict",
|
||||
"regex"
|
||||
],
|
||||
"title": "Matching mode"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Url"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"matching_mode",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"title": "Redirect uris"
|
||||
},
|
||||
"sub_mode": {
|
||||
"type": "string",
|
||||
@ -6974,7 +6995,7 @@
|
||||
"spnego_server_name": {
|
||||
"type": "string",
|
||||
"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": {
|
||||
"type": "string",
|
||||
@ -9781,6 +9802,10 @@
|
||||
"minLength": 1,
|
||||
"title": "Api url"
|
||||
},
|
||||
"interactive": {
|
||||
"type": "boolean",
|
||||
"title": "Interactive"
|
||||
},
|
||||
"score_min_threshold": {
|
||||
"type": "number",
|
||||
"title": "Score min threshold"
|
||||
@ -13383,12 +13408,6 @@
|
||||
"title": "Authorization flow",
|
||||
"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": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
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
|
||||
command: server
|
||||
environment:
|
||||
@ -52,7 +52,7 @@ services:
|
||||
- postgresql
|
||||
- redis
|
||||
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
|
||||
command: worker
|
||||
environment:
|
||||
|
2
go.mod
2
go.mod
@ -29,7 +29,7 @@ require (
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
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/oauth2 v0.23.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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
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.2024100.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024083.13 h1:xKh3feJYUeLw583zZ5ifgV0qjD37ZCOzgXPfbHQSbHM=
|
||||
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-20190510104115-cbcb75029529/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())
|
||||
}
|
||||
|
||||
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)")
|
||||
return err
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting LDAP server")
|
||||
|
@ -48,7 +48,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
|
||||
return err
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
|
||||
tln := tls.NewListener(proxyListener, tlsConfig)
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
"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)))
|
||||
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{
|
||||
Host: externalHost.Host,
|
||||
log: muxLogger,
|
||||
@ -131,7 +140,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
|
||||
tokenVerifier: verifier,
|
||||
proxyConfig: p,
|
||||
httpClient: c,
|
||||
publicHostHTTPClient: web.NewHostInterceptor(c, server.API().Outpost.Config["authentik_host"].(string)),
|
||||
publicHostHTTPClient: publicHTTPClient,
|
||||
mux: mux,
|
||||
errorTemplates: templates.GetTemplates(),
|
||||
ak: server.API(),
|
||||
|
@ -129,7 +129,7 @@ func (ps *ProxyServer) ServeHTTP() {
|
||||
ps.log.WithField("listen", listenAddress).WithError(err).Warning("Failed to listen")
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: listener}
|
||||
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
|
||||
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)")
|
||||
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()
|
||||
|
||||
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) {
|
||||
r.Host = t.host
|
||||
r.Header.Set("X-Forwarded-Proto", t.scheme)
|
||||
if r.Host != t.host {
|
||||
r.Host = t.host
|
||||
r.Header.Set("X-Forwarded-Proto", t.scheme)
|
||||
}
|
||||
return t.inner.RoundTrip(r)
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,15 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@ -14,14 +18,25 @@ import (
|
||||
"goauthentik.io/internal/utils/sentry"
|
||||
)
|
||||
|
||||
const MetricsKeyFile = "authentik-core-metrics.key"
|
||||
|
||||
var Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "authentik_main_request_duration_seconds",
|
||||
Help: "API request latencies in seconds",
|
||||
}, []string{"dest"})
|
||||
|
||||
func (ws *WebServer) runMetricsServer() {
|
||||
m := mux.NewRouter()
|
||||
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.Path("/metrics").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
promhttp.InstrumentMetricHandler(
|
||||
@ -36,7 +51,7 @@ func (ws *WebServer) runMetricsServer() {
|
||||
l.WithError(err).Warning("failed to get upstream metrics")
|
||||
return
|
||||
}
|
||||
re.SetBasicAuth("monitor", config.Get().SecretKey)
|
||||
re.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
|
||||
res, err := ws.upstreamHttpClient().Do(re)
|
||||
if err != nil {
|
||||
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")
|
||||
err := http.ListenAndServe(config.Get().Listen.Metrics, m)
|
||||
err = http.ListenAndServe(config.Get().Listen.Metrics, m)
|
||||
if err != nil {
|
||||
l.WithError(err).Warning("Failed to start 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
|
||||
if config.Get().Storage.Media.Backend == "file" {
|
||||
fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path))
|
||||
staticRouter.PathPrefix("/media/").Handler(http.StripPrefix("/media", fsMedia))
|
||||
fsMedia := http.StripPrefix("/media", http.FileServer(http.Dir(config.Get().Storage.Media.File.Path)))
|
||||
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/"))))
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/gounicorn"
|
||||
"goauthentik.io/internal/outpost/proxyv2"
|
||||
"goauthentik.io/internal/utils"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
"goauthentik.io/internal/web/brand_tls"
|
||||
)
|
||||
@ -52,7 +53,7 @@ func NewWebServer() *WebServer {
|
||||
loggingHandler.Use(web.NewLoggingHandler(l, nil))
|
||||
|
||||
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
|
||||
// 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")
|
||||
return
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
|
||||
defer proxyListener.Close()
|
||||
|
||||
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)")
|
||||
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()
|
||||
|
||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -2614,7 +2614,12 @@ msgid "Captcha Stages"
|
||||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: authentik/stages/captcha/stage.py
|
||||
|
@ -11,17 +11,15 @@
|
||||
# Marco Vitale, 2024
|
||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2024
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2024
|
||||
# Nicola Mersi, 2024
|
||||
# tom max, 2024
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"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"
|
||||
"MIME-Version: 1.0\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)"
|
||||
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
|
||||
msgid ""
|
||||
"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."
|
||||
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
|
||||
msgid "Only a single LDAP Source with password synchronization is allowed"
|
||||
msgstr ""
|
||||
@ -2877,10 +2735,13 @@ msgid "Captcha Stages"
|
||||
msgstr "Fasi Captcha"
|
||||
|
||||
#: authentik/stages/captcha/stage.py
|
||||
msgid "Invalid captcha response. Retrying may solve this issue."
|
||||
msgstr ""
|
||||
"Risposta captcha non valida. Un nuovo tentativo potrebbe risolvere il "
|
||||
"problema."
|
||||
msgid "Unknown error"
|
||||
msgstr "Errore sconosciuto"
|
||||
|
||||
#: 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
|
||||
msgid "Invalid captcha response"
|
||||
@ -3253,10 +3114,6 @@ msgstr "Database utente + password app"
|
||||
msgid "User database + LDAP password"
|
||||
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
|
||||
msgid "Selection of backends to test the password against."
|
||||
msgstr "Selezione di backend su cui testare la password."
|
||||
|
Binary file not shown.
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -2649,8 +2649,13 @@ msgid "Captcha Stages"
|
||||
msgstr "验证码阶段"
|
||||
|
||||
#: authentik/stages/captcha/stage.py
|
||||
msgid "Invalid captcha response. Retrying may solve this issue."
|
||||
msgstr "无效的验证码响应。重试可能会解决此问题。"
|
||||
msgid "Unknown error"
|
||||
msgstr "未知错误"
|
||||
|
||||
#: authentik/stages/captcha/stage.py
|
||||
#, python-brace-format
|
||||
msgid "Failed to validate token: {error}"
|
||||
msgstr "验证令牌失败:{error}"
|
||||
|
||||
#: authentik/stages/captcha/stage.py
|
||||
msgid "Invalid captcha response"
|
||||
|
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -2648,8 +2648,13 @@ msgid "Captcha Stages"
|
||||
msgstr "验证码阶段"
|
||||
|
||||
#: authentik/stages/captcha/stage.py
|
||||
msgid "Invalid captcha response. Retrying may solve this issue."
|
||||
msgstr "无效的验证码响应。重试可能会解决此问题。"
|
||||
msgid "Unknown error"
|
||||
msgstr "未知错误"
|
||||
|
||||
#: authentik/stages/captcha/stage.py
|
||||
#, python-brace-format
|
||||
msgid "Failed to validate token: {error}"
|
||||
msgstr "验证令牌失败:{error}"
|
||||
|
||||
#: authentik/stages/captcha/stage.py
|
||||
msgid "Invalid captcha response"
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2024.10.0",
|
||||
"version": "2024.10.4",
|
||||
"private": true
|
||||
}
|
||||
|
28
poetry.lock
generated
28
poetry.lock
generated
@ -3896,13 +3896,13 @@ pytest = ">=4.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pytest-randomly"
|
||||
version = "3.16.0"
|
||||
version = "3.15.0"
|
||||
description = "Pytest plugin to randomly order tests and control random.seed."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"},
|
||||
{file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"},
|
||||
{file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"},
|
||||
{file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4354,13 +4354,13 @@ django-query = ["django (>=3.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "selenium"
|
||||
version = "4.26.0"
|
||||
version = "4.25.0"
|
||||
description = "Official Python bindings for Selenium WebDriver"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "selenium-4.26.0-py3-none-any.whl", hash = "sha256:48013f36e812de5b3948ef53d04e73f77bc923ee3e1d7d99eaf0618179081b99"},
|
||||
{file = "selenium-4.26.0.tar.gz", hash = "sha256:f0780f85f10310aa5d085b81e79d73d3c93b83d8de121d0400d543a50ee963e8"},
|
||||
{file = "selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33"},
|
||||
{file = "selenium-4.25.0.tar.gz", hash = "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4425,13 +4425,13 @@ tornado = ["tornado (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "service-identity"
|
||||
version = "24.2.0"
|
||||
version = "24.1.0"
|
||||
description = "Service identity verification for pyOpenSSL & cryptography."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85"},
|
||||
{file = "service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09"},
|
||||
{file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"},
|
||||
{file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4441,7 +4441,7 @@ pyasn1 = "*"
|
||||
pyasn1-modules = "*"
|
||||
|
||||
[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"]
|
||||
idna = ["idna"]
|
||||
mypy = ["idna", "mypy", "types-pyopenssl"]
|
||||
@ -4751,13 +4751,13 @@ wsproto = ">=0.14"
|
||||
|
||||
[[package]]
|
||||
name = "twilio"
|
||||
version = "9.3.6"
|
||||
version = "9.3.5"
|
||||
description = "Twilio API client and TwiML generator"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "twilio-9.3.6-py2.py3-none-any.whl", hash = "sha256:c5d7f4cfeb50a7928397b8f819c8f7fb2bb956a1a2cabbda1df1d7a40f9ce1d7"},
|
||||
{file = "twilio-9.3.6.tar.gz", hash = "sha256:d42691f7fe1faaa5ba82942f169bfea4d7f01a0a542a456d82018fb49bd1f5b2"},
|
||||
{file = "twilio-9.3.5-py2.py3-none-any.whl", hash = "sha256:d6a97a77b98cc176a61c960f11894af385bc1c11b93e2e8b79fdfb9601788fb0"},
|
||||
{file = "twilio-9.3.5.tar.gz", hash = "sha256:608d78a903d403465aac1840c58a6546a090b7e222d2bf539a93c3831072880c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2024.10.0"
|
||||
version = "2024.10.4"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
94
schema.yml
94
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2024.10.0
|
||||
version: 2024.10.4
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -20218,10 +20218,6 @@ paths:
|
||||
format: uuid
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: redirect_uris
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: refresh_token_validity
|
||||
schema:
|
||||
@ -20637,10 +20633,6 @@ paths:
|
||||
format: uuid
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: redirect_uris__iexact
|
||||
schema:
|
||||
type: string
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
@ -39220,7 +39212,10 @@ components:
|
||||
type: string
|
||||
js_url:
|
||||
type: string
|
||||
interactive:
|
||||
type: boolean
|
||||
required:
|
||||
- interactive
|
||||
- js_url
|
||||
- pending_user
|
||||
- pending_user_avatar
|
||||
@ -39276,6 +39271,8 @@ components:
|
||||
type: string
|
||||
api_url:
|
||||
type: string
|
||||
interactive:
|
||||
type: boolean
|
||||
score_min_threshold:
|
||||
type: number
|
||||
format: double
|
||||
@ -39322,6 +39319,8 @@ components:
|
||||
api_url:
|
||||
type: string
|
||||
minLength: 1
|
||||
interactive:
|
||||
type: boolean
|
||||
score_min_threshold:
|
||||
type: number
|
||||
format: double
|
||||
@ -42975,7 +42974,8 @@ components:
|
||||
readOnly: true
|
||||
spnego_server_name:
|
||||
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:
|
||||
type: string
|
||||
description: Credential cache to use for SPNEGO in form type:residual
|
||||
@ -43144,7 +43144,8 @@ components:
|
||||
be in the form TYPE:residual
|
||||
spnego_server_name:
|
||||
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:
|
||||
type: string
|
||||
writeOnly: true
|
||||
@ -44051,6 +44052,11 @@ components:
|
||||
required:
|
||||
- challenge
|
||||
- name
|
||||
MatchingModeEnum:
|
||||
enum:
|
||||
- strict
|
||||
- regex
|
||||
type: string
|
||||
Metadata:
|
||||
type: object
|
||||
description: Serializer for blueprint metadata
|
||||
@ -44753,8 +44759,9 @@ components:
|
||||
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
||||
and returned as JWEs.
|
||||
redirect_uris:
|
||||
type: string
|
||||
description: Enter each URI on a new line.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RedirectURI'
|
||||
sub_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SubModeEnum'
|
||||
@ -44783,6 +44790,7 @@ components:
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
- redirect_uris
|
||||
- verbose_name
|
||||
- verbose_name_plural
|
||||
OAuth2ProviderRequest:
|
||||
@ -44854,8 +44862,9 @@ components:
|
||||
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
||||
and returned as JWEs.
|
||||
redirect_uris:
|
||||
type: string
|
||||
description: Enter each URI on a new line.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RedirectURIRequest'
|
||||
sub_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SubModeEnum'
|
||||
@ -44877,6 +44886,7 @@ components:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
- name
|
||||
- redirect_uris
|
||||
OAuth2ProviderSetupURLs:
|
||||
type: object
|
||||
description: OAuth2 Provider Metadata serializer
|
||||
@ -44934,7 +44944,8 @@ components:
|
||||
minLength: 1
|
||||
default: ak-provider-oauth2-device-code
|
||||
code:
|
||||
type: integer
|
||||
type: string
|
||||
minLength: 1
|
||||
required:
|
||||
- code
|
||||
OAuthDeviceCodeFinishChallenge:
|
||||
@ -47730,6 +47741,8 @@ components:
|
||||
api_url:
|
||||
type: string
|
||||
minLength: 1
|
||||
interactive:
|
||||
type: boolean
|
||||
score_min_threshold:
|
||||
type: number
|
||||
format: double
|
||||
@ -48448,7 +48461,8 @@ components:
|
||||
be in the form TYPE:residual
|
||||
spnego_server_name:
|
||||
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:
|
||||
type: string
|
||||
writeOnly: true
|
||||
@ -48871,8 +48885,9 @@ components:
|
||||
description: Key used to encrypt the tokens. When set, tokens will be encrypted
|
||||
and returned as JWEs.
|
||||
redirect_uris:
|
||||
type: string
|
||||
description: Enter each URI on a new line.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RedirectURIRequest'
|
||||
sub_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SubModeEnum'
|
||||
@ -49461,10 +49476,6 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -51469,7 +51480,9 @@ components:
|
||||
description: When enabled, this provider will intercept the authorization
|
||||
header and authenticate requests based on its value.
|
||||
redirect_uris:
|
||||
type: string
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RedirectURI'
|
||||
readOnly: true
|
||||
cookie_domain:
|
||||
type: string
|
||||
@ -51696,10 +51709,6 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -51757,7 +51766,6 @@ components:
|
||||
- assigned_backchannel_application_slug
|
||||
- authorization_flow
|
||||
- component
|
||||
- invalidation_flow
|
||||
- meta_model_name
|
||||
- name
|
||||
- outpost_set
|
||||
@ -51781,10 +51789,6 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used when authorizing this provider.
|
||||
invalidation_flow:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Flow used ending the session from a provider.
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
@ -51801,7 +51805,6 @@ components:
|
||||
description: When set to true, connection tokens will be deleted upon disconnect.
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
- name
|
||||
RadiusCheckAccess:
|
||||
type: object
|
||||
@ -52075,6 +52078,29 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- 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:
|
||||
type: object
|
||||
description: Reputation Serializer
|
||||
|
@ -12,7 +12,12 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.policies.expression.models import ExpressionPolicy
|
||||
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
|
||||
|
||||
|
||||
@ -73,7 +78,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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,
|
||||
)
|
||||
Application.objects.create(
|
||||
@ -128,7 +135,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
@ -199,7 +208,9 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
|
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
||||
SCOPE_OPENID_EMAIL,
|
||||
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
|
||||
|
||||
|
||||
@ -82,7 +88,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
signing_key=create_test_cert(),
|
||||
redirect_uris="http://localhost:3000/",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/")],
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -131,7 +137,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -200,7 +210,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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,
|
||||
invalidation_flow=invalidation_flow,
|
||||
)
|
||||
@ -275,7 +289,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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(
|
||||
ScopeMapping.objects.filter(
|
||||
@ -355,7 +373,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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(
|
||||
ScopeMapping.objects.filter(
|
||||
|
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
||||
SCOPE_OPENID_EMAIL,
|
||||
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
|
||||
|
||||
|
||||
@ -67,7 +73,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
signing_key=create_test_cert(),
|
||||
redirect_uris="http://localhost:9009/",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")],
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -116,7 +122,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -188,7 +196,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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(
|
||||
ScopeMapping.objects.filter(
|
||||
@ -259,7 +269,9 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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(
|
||||
ScopeMapping.objects.filter(
|
||||
|
@ -19,7 +19,13 @@ from authentik.providers.oauth2.constants import (
|
||||
SCOPE_OPENID_EMAIL,
|
||||
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
|
||||
|
||||
|
||||
@ -68,7 +74,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
signing_key=create_test_cert(),
|
||||
redirect_uris="http://localhost:9009/",
|
||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")],
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -117,7 +123,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
signing_key=create_test_cert(),
|
||||
redirect_uris="http://localhost:9009/implicit/",
|
||||
redirect_uris=[
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||
],
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
@ -170,7 +178,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
signing_key=create_test_cert(),
|
||||
redirect_uris="http://localhost:9009/implicit/",
|
||||
redirect_uris=[
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||
],
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
@ -238,7 +248,9 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
signing_key=create_test_cert(),
|
||||
redirect_uris="http://localhost:9009/implicit/",
|
||||
redirect_uris=[
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
|
||||
],
|
||||
)
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
|
@ -66,7 +66,6 @@ const rawCssImportMaps = [
|
||||
'import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";',
|
||||
'import PFTable from "@patternfly/patternfly/components/Table/table.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 PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";',
|
||||
'import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";',
|
||||
|
@ -119,13 +119,22 @@ async function buildOneSource(source, dest) {
|
||||
Date.now() - start
|
||||
}ms`,
|
||||
);
|
||||
return 0;
|
||||
} catch (exc) {
|
||||
console.error(`[${new Date(Date.now()).toISOString()}] Failed to build ${source}: ${exc}`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@ -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")) {
|
||||
// There's no watch-for-proxy, sorry.
|
||||
await buildAuthentik(
|
||||
interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
||||
process.exit(
|
||||
await buildAuthentik(
|
||||
interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
|
||||
),
|
||||
);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// 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",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@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/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@ -84,7 +84,7 @@
|
||||
"@wdio/cli": "^9.1.2",
|
||||
"@wdio/spec-reporter": "^9.1.2",
|
||||
"chokidar": "^4.0.1",
|
||||
"chromedriver": "^129.0.2",
|
||||
"chromedriver": "^130.0.4",
|
||||
"esbuild": "^0.24.0",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-lit": "^1.15.0",
|
||||
@ -1014,9 +1014,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.0.tgz",
|
||||
"integrity": "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw==",
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.2.tgz",
|
||||
"integrity": "sha512-Fq7eWOl1Rcbrfn6jD8FPCj9Auaxdm5nIK5RYOeW7ughnd/rY5AmPg6b+CfsG39ZHdwiwe8lde3q8uR7CF5S0yQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
@ -1775,9 +1775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.10.0-1730331602",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.10.0-1730331602.tgz",
|
||||
"integrity": "sha512-VaXywdDCFkIs9RgmHVYt8jGf5xnc+czsu5ILEThNQOuXvBjkGa0J8aPWVSdfP++GiHnkjddWVFzJ6R6LOoHbWQ=="
|
||||
"version": "2024.10.2-1732206118",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.10.2-1732206118.tgz",
|
||||
"integrity": "sha512-Zg90AJvGDquD3u73yIBKXFBDxsCljPxVqylylS6hgPzkLSogKVVkjhmKteWFXDrVxxsxo5XIa4FuTe3wAERyzw=="
|
||||
},
|
||||
"node_modules/@goauthentik/web": {
|
||||
"resolved": "",
|
||||
@ -1953,9 +1953,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/figures": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz",
|
||||
"integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz",
|
||||
"integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@ -2425,9 +2425,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.4.19",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz",
|
||||
"integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==",
|
||||
"version": "1.4.18",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.18.tgz",
|
||||
"integrity": "sha512-Y8BeHOt4LtcxJgXwadtfSeWPrh0XzklcCHnCVT+vOsxqH4gWmunP2ykX+VVOlM/dusyVyiNfG3lv0f10UK+mgA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
@ -2507,9 +2507,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lit/context": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.3.tgz",
|
||||
"integrity": "sha512-Auh37F4S0PZM93HTDfZWs97mmzaQ7M3vnTc9YvxAGyP3UItSK/8Fs0vTOGT+njuvOwbKio/l8Cx/zWL4vkutpQ==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.2.tgz",
|
||||
"integrity": "sha512-S0nw2C6Tkm7fVX5TGYqeROGD+Z9Coa2iFpW+ysYBDH3YvCqOY3wVQvSgwbaliLJkjTnSEYCBe9qFqKV8WUFpVw==",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.2 || ^2.0.0"
|
||||
}
|
||||
@ -3517,6 +3517,12 @@
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
@ -3673,9 +3679,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@spotlightjs/overlay": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@spotlightjs/overlay/-/overlay-2.6.0.tgz",
|
||||
"integrity": "sha512-UnvWi6J8MTxhUHaBxd+k2mVyQh+0sJ5MImb/UfnEOA0WhNaFl2Foo/R7ByoOIDzDGT42+mZtfEyhJzNZ5naAaQ=="
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@spotlightjs/overlay/-/overlay-2.5.2.tgz",
|
||||
"integrity": "sha512-2YCxkTCRWn9Q4x6Aan0b43EvUJEgQBLvcws1c97LO7Cgnb0iKjiLIRW0Fa8f6PO8r9AcDvaFzhk5Z93YIkwlGQ=="
|
||||
},
|
||||
"node_modules/@spotlightjs/sidecar": {
|
||||
"version": "1.8.0",
|
||||
@ -3694,11 +3700,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@spotlightjs/spotlight": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@spotlightjs/spotlight/-/spotlight-2.5.0.tgz",
|
||||
"integrity": "sha512-tOlefTjOUuNGyHtt1+r1IdY5vq9Hq9Vi2HqPENZ9orS/12KMEK6rVFliWT0/IXJLH5LdunNGaw3kan08bsA+NQ==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@spotlightjs/spotlight/-/spotlight-2.4.2.tgz",
|
||||
"integrity": "sha512-d7wEvG6fyP9OsqgJ3fD2YZ2zjtD9MzIuOpqTg+LAT1FXl7yBK9LRrLW/LU6OvwRdDCzUsbnxUv0giJ5DjV+2Lw==",
|
||||
"dependencies": {
|
||||
"@spotlightjs/overlay": "2.6.0",
|
||||
"@spotlightjs/overlay": "2.5.2",
|
||||
"@spotlightjs/sidecar": "1.8.0",
|
||||
"import-meta-resolve": "^4.1.0"
|
||||
},
|
||||
@ -5879,9 +5885,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mocha": {
|
||||
"version": "10.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.9.tgz",
|
||||
"integrity": "sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==",
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.8.tgz",
|
||||
"integrity": "sha512-HfMcUmy9hTMJh66VNcmeC9iVErIZJli2bszuXc6julh5YGuRb/W5OnkHjwLNYdFlMis0sY3If5SEAp+PktdJjw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mute-stream": {
|
||||
@ -5894,9 +5900,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
||||
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
||||
"version": "22.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
|
||||
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -6077,16 +6083,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz",
|
||||
"integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz",
|
||||
"integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.8.1",
|
||||
"@typescript-eslint/type-utils": "8.8.1",
|
||||
"@typescript-eslint/utils": "8.8.1",
|
||||
"@typescript-eslint/visitor-keys": "8.8.1",
|
||||
"@typescript-eslint/scope-manager": "8.8.0",
|
||||
"@typescript-eslint/type-utils": "8.8.0",
|
||||
"@typescript-eslint/utils": "8.8.0",
|
||||
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@ -6110,15 +6116,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz",
|
||||
"integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz",
|
||||
"integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.8.1",
|
||||
"@typescript-eslint/types": "8.8.1",
|
||||
"@typescript-eslint/typescript-estree": "8.8.1",
|
||||
"@typescript-eslint/visitor-keys": "8.8.1",
|
||||
"@typescript-eslint/scope-manager": "8.8.0",
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"@typescript-eslint/typescript-estree": "8.8.0",
|
||||
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@ -6138,13 +6144,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz",
|
||||
"integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz",
|
||||
"integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.8.1",
|
||||
"@typescript-eslint/visitor-keys": "8.8.1"
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"@typescript-eslint/visitor-keys": "8.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@ -6155,13 +6161,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz",
|
||||
"integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz",
|
||||
"integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.8.1",
|
||||
"@typescript-eslint/utils": "8.8.1",
|
||||
"@typescript-eslint/typescript-estree": "8.8.0",
|
||||
"@typescript-eslint/utils": "8.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@ -6179,9 +6185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz",
|
||||
"integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz",
|
||||
"integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@ -6192,13 +6198,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz",
|
||||
"integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz",
|
||||
"integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.8.1",
|
||||
"@typescript-eslint/visitor-keys": "8.8.1",
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@ -6220,15 +6226,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz",
|
||||
"integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz",
|
||||
"integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.8.1",
|
||||
"@typescript-eslint/types": "8.8.1",
|
||||
"@typescript-eslint/typescript-estree": "8.8.1"
|
||||
"@typescript-eslint/scope-manager": "8.8.0",
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"@typescript-eslint/typescript-estree": "8.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@ -6242,12 +6248,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz",
|
||||
"integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz",
|
||||
"integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.8.1",
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -6843,9 +6849,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/cli/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -6971,9 +6977,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/local-runner/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -7024,9 +7030,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/mocha-framework/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -7051,9 +7057,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/repl/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -7076,9 +7082,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/reporter/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -7107,9 +7113,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/runner/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -7156,9 +7162,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@wdio/types/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -8693,9 +8699,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromedriver": {
|
||||
"version": "129.0.2",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-129.0.2.tgz",
|
||||
"integrity": "sha512-rUEFCJAmAwOdFfaDFtveT97fFeA7NOxlkgyPyN+G09Ws4qGW39aLDxMQBbS9cxQQHhTihqZZobgF5CLVYXnmGA==",
|
||||
"version": "130.0.4",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-130.0.4.tgz",
|
||||
"integrity": "sha512-lpR+PWXszij1k4Ig3t338Zvll9HtCTiwoLM7n4pCCswALHxzmgwaaIFBh3rt9+5wRk9D07oFblrazrBxwaYYAQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@ -9005,9 +9011,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz",
|
||||
"integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA=="
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.2.3",
|
||||
@ -9052,9 +9058,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@ -9855,9 +9861,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge-ts": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.3.tgz",
|
||||
"integrity": "sha512-qCSH6I0INPxd9Y1VtAiLpnYvz5O//6rCfJXKk0z66Up9/VOSr+1yS8XSKA5IWRxjocFGlzPyaZYe+jxq7OOLtQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.1.tgz",
|
||||
"integrity": "sha512-M27OAbyR/XgJujhAd6ZlYvZGzejbzvGPSZWwuzezPCdKLT9VMtK0kpRNDc5LeUDYqFN3e254gWG1yKpjidCtow==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@ -10272,9 +10278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.33",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz",
|
||||
"integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==",
|
||||
"version": "1.5.32",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz",
|
||||
"integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@ -11032,16 +11038,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/eslint-plugin-wc": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-2.2.0.tgz",
|
||||
"integrity": "sha512-kjPp+aXz23fOl0JZJOJS+6adwhEv98KjZ2FJqWpc4vtmk4Oenz/JJmmNZrGSARgtyR0BLIF/kVWC6GSlHA+5MA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-2.1.1.tgz",
|
||||
"integrity": "sha512-GfJo05ZgWfwAFbW6Gkf+9CMOIU6fmbd3b4nm+PKESHgUdUTmi7vawlELCrzOhdiQjXUPZxDfFIVxYt9D/v/GdQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-valid-element-name": "^1.0.0",
|
||||
"js-levenshtein-esm": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8.40.0"
|
||||
"eslint": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
@ -11405,9 +11411,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
@ -11415,7 +11421,7 @@
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@ -14293,9 +14299,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "5.33.1",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.33.1.tgz",
|
||||
"integrity": "sha512-SeuH+6IcDNNFRdVsi2uGnO6gsSDlx1V+TcQkKnzciF2Z7QHHasKseGUf9GMNVm3bSCKDeqaKPMp0F6BOiKuYRA==",
|
||||
"version": "5.31.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.31.0.tgz",
|
||||
"integrity": "sha512-4hR+qHx/id7mniCWWUqA4MXwGjYFN75xv3qLmEkl9Hm6eCKAhv0wGP0CyrXKUYxVyDplJQsqQaAlsjuRKYsdPA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -14317,7 +14323,7 @@
|
||||
"easy-table": "1.2.0",
|
||||
"enhanced-resolve": "^5.17.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"jiti": "^2.3.3",
|
||||
"jiti": "^2.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"picocolors": "^1.0.0",
|
||||
@ -14342,9 +14348,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/jiti": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz",
|
||||
"integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.2.1.tgz",
|
||||
"integrity": "sha512-weIl/Bv3G0J3UKamLxEA2G+FfQ33Z1ZkQJGPjKFV21zQdKWu2Pi6o4elpj2uEl5XdFJZ9xzn1fsanWTFSt45zw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@ -14489,9 +14495,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
|
||||
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz",
|
||||
"integrity": "sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw==",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"lit-element": "^4.1.0",
|
||||
@ -14525,9 +14531,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lit-element": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz",
|
||||
"integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.0.tgz",
|
||||
"integrity": "sha512-gSejRUQJuMQjV2Z59KAS/D4iElUhwKpIyJvZ9w+DIagIQjfJnhR20h2Q5ddpzXGS+fF0tMZ/xEYGMnKmaI/iww==",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.2.0",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
@ -14535,9 +14541,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lit-html": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz",
|
||||
"integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.0.tgz",
|
||||
"integrity": "sha512-pwT/HwoxqI9FggTrYVarkBKFN9MlTUpLrDHubTmW4SrkL3kkqW5gxwbxMMUnbbRHBC0WTZnYHcjDSCM559VyfA==",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
@ -15201,14 +15207,14 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz",
|
||||
"integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz",
|
||||
"integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==",
|
||||
"dependencies": {
|
||||
"acorn": "^8.12.1",
|
||||
"acorn": "^8.11.3",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.2.0",
|
||||
"ufo": "^1.5.4"
|
||||
"pkg-types": "^1.1.1",
|
||||
"ufo": "^1.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
@ -16586,9 +16592,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/package-manager-detector": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.1.tgz",
|
||||
"integrity": "sha512-/hVW2fZvAdEas+wyKh0SnlZ2mx0NIa1+j11YaQkogEJkcMErbwchHCuo8z7lEtajZJQZ6rgZNVTWMVVd71Bjng=="
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.0.tgz",
|
||||
"integrity": "sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog=="
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
@ -16900,12 +16906,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz",
|
||||
"integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz",
|
||||
"integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.2",
|
||||
"confbox": "^0.1.7",
|
||||
"mlly": "^1.7.1",
|
||||
"pathe": "^1.1.2"
|
||||
}
|
||||
},
|
||||
@ -19538,11 +19544,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-client": {
|
||||
"version": "3.29.4",
|
||||
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.29.4.tgz",
|
||||
"integrity": "sha512-Me8tdPyRAQbnwNBCZ0BpG0vyci9e+FW6YV3+c6/x8SwPmLpslpFNXoT4PtVApf1CVSvV7Sc7Bfb4DPgpEqBdHw==",
|
||||
"version": "3.31.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.31.0.tgz",
|
||||
"integrity": "sha512-hVYift5XB8nOgNJVl6cbNtVTVPT2Fdx2wCOcIvuAFcyq0Mwe6+70ezoZ5WfiaIAzzwWfq72jyaLeg8TViGNSmw==",
|
||||
"dependencies": {
|
||||
"@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-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",
|
||||
@ -20646,14 +20653,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.1.tgz",
|
||||
"integrity": "sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.0.tgz",
|
||||
"integrity": "sha512-BjIT/VwJ8+0rVO01ZQ2ZVnjE1svFBiRczcpr1t1Yxt7sT25VSbPfrJtDsQ8uQTy2pilX5nI9gwxhUyLULNentw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.8.1",
|
||||
"@typescript-eslint/parser": "8.8.1",
|
||||
"@typescript-eslint/utils": "8.8.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.8.0",
|
||||
"@typescript-eslint/parser": "8.8.0",
|
||||
"@typescript-eslint/utils": "8.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@ -20911,17 +20918,17 @@
|
||||
"integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg=="
|
||||
},
|
||||
"node_modules/untyped": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.1.tgz",
|
||||
"integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.0.tgz",
|
||||
"integrity": "sha512-o2Vjmn2dal08BzCcINxSmWuAteReUUiXseii5VRhmxyLF0b21K0iKZQ9fMYK7RWspVkY+0saqaVQNq4roe3Efg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.7",
|
||||
"@babel/standalone": "^7.25.7",
|
||||
"@babel/types": "^7.25.7",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/standalone": "^7.25.6",
|
||||
"@babel/types": "^7.25.6",
|
||||
"defu": "^6.1.4",
|
||||
"jiti": "^2.3.1",
|
||||
"jiti": "^2.0.0",
|
||||
"mri": "^1.2.0",
|
||||
"scule": "^1.3.0"
|
||||
},
|
||||
@ -20945,9 +20952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/untyped/node_modules/jiti": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz",
|
||||
"integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.2.1.tgz",
|
||||
"integrity": "sha512-weIl/Bv3G0J3UKamLxEA2G+FfQ33Z1ZkQJGPjKFV21zQdKWu2Pi6o4elpj2uEl5XdFJZ9xzn1fsanWTFSt45zw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"bin": {
|
||||
@ -21881,9 +21888,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webdriver/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -21936,9 +21943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webdriverio/node_modules/@types/node": {
|
||||
"version": "20.16.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
|
@ -11,7 +11,7 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@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/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@ -72,7 +72,7 @@
|
||||
"@wdio/cli": "^9.1.2",
|
||||
"@wdio/spec-reporter": "^9.1.2",
|
||||
"chokidar": "^4.0.1",
|
||||
"chromedriver": "^129.0.2",
|
||||
"chromedriver": "^130.0.4",
|
||||
"esbuild": "^0.24.0",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-lit": "^1.15.0",
|
||||
@ -328,12 +328,18 @@
|
||||
},
|
||||
"test:e2e:watch": {
|
||||
"command": "wdio run ./tests/wdio.conf.ts",
|
||||
"dependencies": [
|
||||
"build"
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
|
||||
}
|
||||
},
|
||||
"test:watch": {
|
||||
"command": "wdio run ./wdio.conf.ts",
|
||||
"dependencies": [
|
||||
"build"
|
||||
],
|
||||
"env": {
|
||||
"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