mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 15:42:48 +02:00
Compare commits
47 Commits
ui-tidy
...
enforce-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc794d927e | ||
|
|
01b91e9dd1 | ||
|
|
6efb3f5fb0 | ||
|
|
4b24a7ddc1 | ||
|
|
b6b423bee9 | ||
|
|
860a43e56a | ||
|
|
c1cbd0623c | ||
|
|
0893031cab | ||
|
|
89868276da | ||
|
|
dd40f60c88 | ||
|
|
73be48c179 | ||
|
|
fc27dfeeb7 | ||
|
|
dbbfb3cf19 | ||
|
|
6d7249ea56 | ||
|
|
a07e820bce | ||
|
|
31186baf25 | ||
|
|
024e6c1961 | ||
|
|
1244a40ffb | ||
|
|
dcfe722f5c | ||
|
|
6b1171aac8 | ||
|
|
b2d5519611 | ||
|
|
1620a96cd4 | ||
|
|
a42fc4b741 | ||
|
|
9b822ce0fd | ||
|
|
05c30af790 | ||
|
|
6683d9943c | ||
|
|
17ef75c19f | ||
|
|
d8428bf59a | ||
|
|
3ef06094b5 | ||
|
|
6b22487406 | ||
|
|
0fa412e782 | ||
|
|
334c0175f9 | ||
|
|
3c2f39559f | ||
|
|
d05ad4403b | ||
|
|
10866f9dfc | ||
|
|
97f0c6475d | ||
|
|
0f6cb9183e | ||
|
|
499c1b6fab | ||
|
|
362d67ca6e | ||
|
|
abe944b8c9 | ||
|
|
bba9643864 | ||
|
|
467af902f1 | ||
|
|
e28a8aacc7 | ||
|
|
af0444b0dd | ||
|
|
8fcf60ecce | ||
|
|
10ebbcfd61 | ||
|
|
6a1bde1fd8 |
@@ -74,7 +74,7 @@ jobs:
|
||||
mkdir -p ./gen-go-api
|
||||
- name: Setup node
|
||||
if: ${{ !inputs.release }}
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
|
||||
2
.github/workflows/api-ts-publish.yml
vendored
2
.github/workflows/api-ts-publish.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
4
.github/workflows/ci-api-docs.yml
vendored
4
.github/workflows/ci-api-docs.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
|
||||
2
.github/workflows/ci-aws-cfn.yml
vendored
2
.github/workflows/ci-aws-cfn.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: lifecycle/aws/package.json
|
||||
cache: "npm"
|
||||
|
||||
4
.github/workflows/ci-docs.yml
vendored
4
.github/workflows/ci-docs.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
|
||||
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@@ -151,7 +151,7 @@ jobs:
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
|
||||
6
.github/workflows/ci-web.yml
vendored
6
.github/workflows/ci-web.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
project: web
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
cache: "npm"
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
|
||||
2
.github/workflows/packages-npm-publish.yml
vendored
2
.github/workflows/packages-npm-publish.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: ${{ matrix.package }}/package.json
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
2
.github/workflows/release-publish.yml
vendored
2
.github/workflows/release-publish.yml
vendored
@@ -150,7 +150,7 @@ jobs:
|
||||
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
|
||||
@@ -42,68 +42,6 @@ def validate_auth(header: bytes, format="bearer") -> str | None:
|
||||
return auth_credentials
|
||||
|
||||
|
||||
def bearer_auth(raw_header: bytes) -> User | None:
|
||||
"""raw_header in the Format of `Bearer ....`"""
|
||||
user = auth_user_lookup(raw_header)
|
||||
if not user:
|
||||
return None
|
||||
if not user.is_active:
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
return user
|
||||
|
||||
|
||||
def auth_user_lookup(raw_header: bytes) -> User | None:
|
||||
"""raw_header in the Format of `Bearer ....`"""
|
||||
from authentik.providers.oauth2.models import AccessToken
|
||||
|
||||
auth_credentials = validate_auth(raw_header)
|
||||
if not auth_credentials:
|
||||
return None
|
||||
# first, check traditional tokens
|
||||
key_token = Token.filter_not_expired(
|
||||
key=auth_credentials, intent=TokenIntents.INTENT_API
|
||||
).first()
|
||||
if key_token:
|
||||
CTX_AUTH_VIA.set("api_token")
|
||||
return key_token.user
|
||||
# then try to auth via JWT
|
||||
jwt_token = AccessToken.filter_not_expired(
|
||||
token=auth_credentials, _scope__icontains=SCOPE_AUTHENTIK_API
|
||||
).first()
|
||||
if jwt_token:
|
||||
# Double-check scopes, since they are saved in a single string
|
||||
# we want to check the parsed version too
|
||||
if SCOPE_AUTHENTIK_API not in jwt_token.scope:
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
CTX_AUTH_VIA.set("jwt")
|
||||
return jwt_token.user
|
||||
# then try to auth via secret key (for embedded outpost/etc)
|
||||
user = token_secret_key(auth_credentials)
|
||||
if user:
|
||||
CTX_AUTH_VIA.set("secret_key")
|
||||
return user
|
||||
# then try to auth via secret key (for embedded outpost/etc)
|
||||
user = token_ipc(auth_credentials)
|
||||
if user:
|
||||
CTX_AUTH_VIA.set("ipc")
|
||||
return user
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
|
||||
|
||||
def token_secret_key(value: str) -> User | None:
|
||||
"""Check if the token is the secret key
|
||||
and return the service account for the managed outpost"""
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
|
||||
if not compare_digest(value, settings.SECRET_KEY):
|
||||
return None
|
||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||
if not outposts:
|
||||
return None
|
||||
outpost = outposts.first()
|
||||
return outpost.user
|
||||
|
||||
|
||||
class IPCUser(AnonymousUser):
|
||||
"""'Virtual' user for IPC communication between authentik core and the authentik router"""
|
||||
|
||||
@@ -133,14 +71,6 @@ class IPCUser(AnonymousUser):
|
||||
return True
|
||||
|
||||
|
||||
def token_ipc(value: str) -> User | None:
|
||||
"""Check if the token is the secret key
|
||||
and return the service account for the managed outpost"""
|
||||
if not ipc_key or not compare_digest(value, ipc_key):
|
||||
return None
|
||||
return IPCUser()
|
||||
|
||||
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
|
||||
@@ -148,12 +78,79 @@ class TokenAuthentication(BaseAuthentication):
|
||||
"""Token-based authentication using HTTP Bearer authentication"""
|
||||
auth = get_authorization_header(request)
|
||||
|
||||
user = bearer_auth(auth)
|
||||
user_ctx = self.bearer_auth(auth)
|
||||
# None is only returned when the header isn't set.
|
||||
if not user:
|
||||
if not user_ctx:
|
||||
return None
|
||||
|
||||
return (user, None) # pragma: no cover
|
||||
return user_ctx
|
||||
|
||||
def bearer_auth(self, raw_header: bytes) -> tuple[User, Any] | None:
|
||||
"""raw_header in the Format of `Bearer ....`"""
|
||||
user_ctx = self.auth_user_lookup(raw_header)
|
||||
if not user_ctx:
|
||||
return None
|
||||
user, ctx = user_ctx
|
||||
if not user.is_active:
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
return user, ctx
|
||||
|
||||
def auth_user_lookup(self, raw_header: bytes) -> tuple[User, Any] | None:
|
||||
"""raw_header in the Format of `Bearer ....`"""
|
||||
from authentik.providers.oauth2.models import AccessToken
|
||||
|
||||
auth_credentials = validate_auth(raw_header)
|
||||
if not auth_credentials:
|
||||
return None
|
||||
# first, check traditional tokens
|
||||
key_token = Token.filter_not_expired(
|
||||
key=auth_credentials, intent=TokenIntents.INTENT_API
|
||||
).first()
|
||||
if key_token:
|
||||
CTX_AUTH_VIA.set("api_token")
|
||||
return key_token.user, key_token
|
||||
# then try to auth via JWT
|
||||
jwt_token = AccessToken.filter_not_expired(
|
||||
token=auth_credentials, _scope__icontains=SCOPE_AUTHENTIK_API
|
||||
).first()
|
||||
if jwt_token:
|
||||
# Double-check scopes, since they are saved in a single string
|
||||
# we want to check the parsed version too
|
||||
if SCOPE_AUTHENTIK_API not in jwt_token.scope:
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
CTX_AUTH_VIA.set("jwt")
|
||||
return jwt_token.user, jwt_token
|
||||
# then try to auth via secret key (for embedded outpost/etc)
|
||||
user_outpost = self.token_secret_key(auth_credentials)
|
||||
if user_outpost:
|
||||
CTX_AUTH_VIA.set("secret_key")
|
||||
return user_outpost
|
||||
# then try to auth via secret key (for embedded outpost/etc)
|
||||
user = self.token_ipc(auth_credentials)
|
||||
if user:
|
||||
CTX_AUTH_VIA.set("ipc")
|
||||
return user
|
||||
raise AuthenticationFailed("Token invalid/expired")
|
||||
|
||||
def token_ipc(self, value: str) -> tuple[User, None] | None:
|
||||
"""Check if the token is the secret key
|
||||
and return the service account for the managed outpost"""
|
||||
if not ipc_key or not compare_digest(value, ipc_key):
|
||||
return None
|
||||
return IPCUser(), None
|
||||
|
||||
def token_secret_key(self, value: str) -> tuple[User, Outpost] | None:
|
||||
"""Check if the token is the secret key
|
||||
and return the service account for the managed outpost"""
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
|
||||
if not compare_digest(value, settings.SECRET_KEY):
|
||||
return None
|
||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||
if not outposts:
|
||||
return None
|
||||
outpost = outposts.first()
|
||||
return outpost.user, outpost
|
||||
|
||||
|
||||
class TokenSchema(OpenApiAuthenticationExtension):
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
import json
|
||||
from base64 import b64encode
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
from authentik.api.authentication import bearer_auth
|
||||
from authentik.api.authentication import IPCUser, TokenAuthentication
|
||||
from authentik.blueprints.tests import reconcile_app
|
||||
from authentik.core.models import Token, TokenIntents, User, UserTypes
|
||||
from authentik.core.models import Token, TokenIntents, UserTypes
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
@@ -24,22 +25,24 @@ class TestAPIAuth(TestCase):
|
||||
|
||||
def test_invalid_type(self):
|
||||
"""Test invalid type"""
|
||||
self.assertIsNone(bearer_auth(b"foo bar"))
|
||||
self.assertIsNone(TokenAuthentication().bearer_auth(b"foo bar"))
|
||||
|
||||
def test_invalid_empty(self):
|
||||
"""Test invalid type"""
|
||||
self.assertIsNone(bearer_auth(b"Bearer "))
|
||||
self.assertIsNone(bearer_auth(b""))
|
||||
self.assertIsNone(TokenAuthentication().bearer_auth(b"Bearer "))
|
||||
self.assertIsNone(TokenAuthentication().bearer_auth(b""))
|
||||
|
||||
def test_invalid_no_token(self):
|
||||
"""Test invalid with no token"""
|
||||
auth = b64encode(b":abc").decode()
|
||||
self.assertIsNone(bearer_auth(f"Basic :{auth}".encode()))
|
||||
self.assertIsNone(TokenAuthentication().bearer_auth(f"Basic :{auth}".encode()))
|
||||
|
||||
def test_bearer_valid(self):
|
||||
"""Test valid token"""
|
||||
token = Token.objects.create(intent=TokenIntents.INTENT_API, user=create_test_admin_user())
|
||||
self.assertEqual(bearer_auth(f"Bearer {token.key}".encode()), token.user)
|
||||
user, tk = TokenAuthentication().bearer_auth(f"Bearer {token.key}".encode())
|
||||
self.assertEqual(user, token.user)
|
||||
self.assertEqual(token, token)
|
||||
|
||||
def test_bearer_valid_deactivated(self):
|
||||
"""Test valid token"""
|
||||
@@ -48,7 +51,7 @@ class TestAPIAuth(TestCase):
|
||||
user.save()
|
||||
token = Token.objects.create(intent=TokenIntents.INTENT_API, user=user)
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
bearer_auth(f"Bearer {token.key}".encode())
|
||||
TokenAuthentication().bearer_auth(f"Bearer {token.key}".encode())
|
||||
|
||||
@reconcile_app("authentik_outposts")
|
||||
def test_managed_outpost_fail(self):
|
||||
@@ -57,20 +60,21 @@ class TestAPIAuth(TestCase):
|
||||
outpost.user.delete()
|
||||
outpost.delete()
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
||||
TokenAuthentication().bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
||||
|
||||
@reconcile_app("authentik_outposts")
|
||||
def test_managed_outpost_success(self):
|
||||
"""Test managed outpost"""
|
||||
user: User = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
||||
user, outpost = TokenAuthentication().bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
|
||||
self.assertEqual(user.type, UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
self.assertEqual(outpost, Outpost.objects.filter(managed=MANAGED_OUTPOST).first())
|
||||
|
||||
def test_jwt_valid(self):
|
||||
"""Test valid JWT"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(), client_id=generate_id(), authorization_flow=create_test_flow()
|
||||
)
|
||||
refresh = AccessToken.objects.create(
|
||||
access = AccessToken.objects.create(
|
||||
user=create_test_admin_user(),
|
||||
provider=provider,
|
||||
token=generate_id(),
|
||||
@@ -78,14 +82,16 @@ class TestAPIAuth(TestCase):
|
||||
_scope=SCOPE_AUTHENTIK_API,
|
||||
_id_token=json.dumps({}),
|
||||
)
|
||||
self.assertEqual(bearer_auth(f"Bearer {refresh.token}".encode()), refresh.user)
|
||||
user, token = TokenAuthentication().bearer_auth(f"Bearer {access.token}".encode())
|
||||
self.assertEqual(user, access.user)
|
||||
self.assertEqual(token, access)
|
||||
|
||||
def test_jwt_missing_scope(self):
|
||||
"""Test valid JWT"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(), client_id=generate_id(), authorization_flow=create_test_flow()
|
||||
)
|
||||
refresh = AccessToken.objects.create(
|
||||
access = AccessToken.objects.create(
|
||||
user=create_test_admin_user(),
|
||||
provider=provider,
|
||||
token=generate_id(),
|
||||
@@ -94,4 +100,12 @@ class TestAPIAuth(TestCase):
|
||||
_id_token=json.dumps({}),
|
||||
)
|
||||
with self.assertRaises(AuthenticationFailed):
|
||||
self.assertEqual(bearer_auth(f"Bearer {refresh.token}".encode()), refresh.user)
|
||||
TokenAuthentication().bearer_auth(f"Bearer {access.token}".encode())
|
||||
|
||||
def test_ipc(self):
|
||||
"""Test IPC auth (mock key)"""
|
||||
key = generate_id()
|
||||
with patch("authentik.api.authentication.ipc_key", key):
|
||||
user, ctx = TokenAuthentication().bearer_auth(f"Bearer {key}".encode())
|
||||
self.assertEqual(user, IPCUser())
|
||||
self.assertEqual(ctx, None)
|
||||
|
||||
62
authentik/api/tests/test_view_authn_authz.py
Normal file
62
authentik/api/tests/test_view_authn_authz.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from collections.abc import Callable
|
||||
from inspect import getmembers
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class TestAPIViewAuthnAuthz(APITestCase): ...
|
||||
|
||||
|
||||
def api_viewset_action(viewset: GenericViewSet, member: Callable) -> Callable:
|
||||
"""Test API Viewset action"""
|
||||
|
||||
def tester(self: TestAPIViewAuthnAuthz):
|
||||
if "permission_classes" in member.kwargs:
|
||||
self.assertNotEqual(
|
||||
member.kwargs["permission_classes"], [], "permission_classes should not be empty"
|
||||
)
|
||||
if "authentication_classes" in member.kwargs:
|
||||
self.assertNotEqual(
|
||||
member.kwargs["authentication_classes"],
|
||||
[],
|
||||
"authentication_classes should not be empty",
|
||||
)
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
def api_view(view: APIView) -> Callable:
|
||||
|
||||
def tester(self: TestAPIViewAuthnAuthz):
|
||||
self.assertNotEqual(view.permission_classes, [], "permission_classes should not be empty")
|
||||
self.assertNotEqual(
|
||||
view.authentication_classes,
|
||||
[],
|
||||
"authentication_classes should not be empty",
|
||||
)
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
# Tell django to load all URLs
|
||||
reverse("authentik_core:root-redirect")
|
||||
for viewset in all_subclasses(GenericViewSet):
|
||||
for act_name, member in getmembers(viewset(), lambda x: isinstance(x, Callable)):
|
||||
if not hasattr(member, "kwargs") or not hasattr(member, "mapping"):
|
||||
continue
|
||||
setattr(
|
||||
TestAPIViewAuthnAuthz,
|
||||
f"test_viewset_{viewset.__name__}_action_{act_name}",
|
||||
api_viewset_action(viewset, member),
|
||||
)
|
||||
for view in all_subclasses(APIView):
|
||||
setattr(
|
||||
TestAPIViewAuthnAuthz,
|
||||
f"test_view_{view.__name__}",
|
||||
api_view(view),
|
||||
)
|
||||
@@ -72,7 +72,7 @@ class AdminDeviceViewSet(ViewSet):
|
||||
"""Viewset for authenticator devices"""
|
||||
|
||||
serializer_class = DeviceSerializer
|
||||
permission_classes = []
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_devices(self, **kwargs):
|
||||
"""Get all devices in all child classes"""
|
||||
|
||||
@@ -17,6 +17,7 @@ from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ListSerializer, ValidationError
|
||||
@@ -296,7 +297,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
methods=["POST"],
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[],
|
||||
permission_classes=[IsAuthenticated],
|
||||
)
|
||||
@validate(UserAccountSerializer)
|
||||
def add_user(self, request: Request, body: UserAccountSerializer, pk: str) -> Response:
|
||||
@@ -327,7 +328,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
methods=["POST"],
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[],
|
||||
permission_classes=[IsAuthenticated],
|
||||
)
|
||||
@validate(UserAccountSerializer)
|
||||
def remove_user(self, request: Request, body: UserAccountSerializer, pk: str) -> Response:
|
||||
|
||||
@@ -43,6 +43,7 @@ from rest_framework.fields import (
|
||||
ListField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
@@ -632,7 +633,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["POST"], permission_classes=[])
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["POST"],
|
||||
permission_classes=[IsAuthenticated],
|
||||
)
|
||||
@validate(UserPasswordSetSerializer)
|
||||
def set_password(self, request: Request, pk: int, body: UserPasswordSetSerializer) -> Response:
|
||||
"""Set password for user"""
|
||||
@@ -718,7 +723,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
204: OpenApiResponse(description="Successfully started impersonation"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["POST"], permission_classes=[])
|
||||
@action(detail=True, methods=["POST"], permission_classes=[IsAuthenticated])
|
||||
def impersonate(self, request: Request, pk: int) -> Response:
|
||||
"""Impersonate a user"""
|
||||
if not request.tenant.impersonation:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""authentik core signals"""
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.contrib.auth.signals import user_logged_in
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
@@ -17,6 +19,8 @@ from authentik.core.models import (
|
||||
User,
|
||||
default_token_duration,
|
||||
)
|
||||
from authentik.flows.apps import RefreshOtherFlowsAfterAuthentication
|
||||
from authentik.root.ws.consumer import build_device_group
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
@@ -47,6 +51,16 @@ def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
|
||||
if session:
|
||||
session.save()
|
||||
|
||||
if not RefreshOtherFlowsAfterAuthentication().get():
|
||||
return
|
||||
layer = get_channel_layer()
|
||||
device_cookie = request.COOKIES.get("authentik_device")
|
||||
if device_cookie:
|
||||
async_to_sync(layer.group_send)(
|
||||
build_device_group(device_cookie),
|
||||
{"type": "event.session.authenticated"},
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=AuthenticatedSession)
|
||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||
|
||||
@@ -28,8 +28,8 @@ from authentik.core.views.interface import (
|
||||
)
|
||||
from authentik.flows.views.interface import FlowInterfaceView
|
||||
from authentik.root.asgi_middleware import AuthMiddlewareStack
|
||||
from authentik.root.messages.consumer import MessageConsumer
|
||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||
from authentik.root.ws.consumer import MessageConsumer
|
||||
from authentik.tenants.channels import TenantsAwareMiddleware
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -27,6 +27,7 @@ from rest_framework.fields import (
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.validators import UniqueValidator
|
||||
@@ -42,7 +43,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
|
||||
from authentik.crypto.models import CertificateKeyPair, KeyType
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.filters import ObjectFilter, SecretKeyFilter
|
||||
from authentik.rbac.filters import SecretKeyFilter
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -292,6 +293,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
@permission_required("view_certificatekeypair_certificate")
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
@@ -302,7 +304,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
],
|
||||
responses={200: CertificateDataSerializer(many=False)},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
|
||||
@action(detail=True, pagination_class=None, permission_classes=[IsAuthenticated])
|
||||
def view_certificate(self, request: Request, pk: str) -> Response:
|
||||
"""Return certificate-key pairs certificate and log access"""
|
||||
certificate: CertificateKeyPair = self.get_object()
|
||||
@@ -323,6 +325,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
return response
|
||||
return Response(CertificateDataSerializer({"data": certificate.certificate_data}).data)
|
||||
|
||||
@permission_required("view_certificatekeypair_key")
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
@@ -333,7 +336,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
],
|
||||
responses={200: CertificateDataSerializer(many=False)},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
|
||||
@action(detail=True, pagination_class=None, permission_classes=[IsAuthenticated])
|
||||
def view_private_key(self, request: Request, pk: str) -> Response:
|
||||
"""Return certificate-key pairs private key and log access"""
|
||||
certificate: CertificateKeyPair = self.get_object()
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-20 14:50
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0004_alter_certificatekeypair_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="certificatekeypair",
|
||||
options={
|
||||
"permissions": [
|
||||
(
|
||||
"view_certificatekeypair_certificate",
|
||||
"View Certificate-Key pair's certificate",
|
||||
),
|
||||
("view_certificatekeypair_key", "View Certificate-Key pair's private key"),
|
||||
],
|
||||
"verbose_name": "Certificate-Key Pair",
|
||||
"verbose_name_plural": "Certificate-Key Pairs",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from binascii import hexlify
|
||||
from hashlib import md5
|
||||
from ssl import PEM_FOOTER, PEM_HEADER
|
||||
from textwrap import wrap
|
||||
from uuid import uuid4
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
@@ -25,6 +27,11 @@ from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def format_cert(raw_pam: str) -> str:
|
||||
"""Format a PEM certificate that is either missing its header/footer or is in a single line"""
|
||||
return "\n".join([PEM_HEADER, *wrap(raw_pam.replace("\n", ""), 64), PEM_FOOTER])
|
||||
|
||||
|
||||
class KeyType(models.TextChoices):
|
||||
"""Cryptographic key algorithm types"""
|
||||
|
||||
@@ -140,3 +147,7 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
class Meta:
|
||||
verbose_name = _("Certificate-Key Pair")
|
||||
verbose_name_plural = _("Certificate-Key Pairs")
|
||||
permissions = [
|
||||
("view_certificatekeypair_certificate", _("View Certificate-Key pair's certificate")),
|
||||
("view_certificatekeypair_key", _("View Certificate-Key pair's private key")),
|
||||
]
|
||||
|
||||
@@ -9,10 +9,16 @@ from cryptography.x509.extensions import SubjectAlternativeName
|
||||
from cryptography.x509.general_name import DNSName
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import assign_perm
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.api.used_by import DeleteAction
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.core.tests.utils import (
|
||||
create_test_admin_user,
|
||||
create_test_cert,
|
||||
create_test_flow,
|
||||
create_test_user,
|
||||
)
|
||||
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
@@ -144,7 +150,7 @@ class TestCrypto(APITestCase):
|
||||
),
|
||||
data={"name": cert.name},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content.decode())
|
||||
api_cert = [x for x in body["results"] if x["name"] == cert.name][0]
|
||||
self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1)
|
||||
@@ -162,7 +168,7 @@ class TestCrypto(APITestCase):
|
||||
),
|
||||
data={"name": cert.name, "has_key": False},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content.decode())
|
||||
api_cert = [x for x in body["results"] if x["name"] == cert.name][0]
|
||||
self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1)
|
||||
@@ -178,7 +184,7 @@ class TestCrypto(APITestCase):
|
||||
),
|
||||
data={"name": cert.name, "include_details": False},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content.decode())
|
||||
api_cert = [x for x in body["results"] if x["name"] == cert.name][0]
|
||||
self.assertEqual(api_cert["fingerprint_sha1"], None)
|
||||
@@ -186,15 +192,18 @@ class TestCrypto(APITestCase):
|
||||
|
||||
def test_certificate_download(self):
|
||||
"""Test certificate export (download)"""
|
||||
self.client.force_login(create_test_admin_user())
|
||||
keypair = create_test_cert()
|
||||
user = create_test_user()
|
||||
assign_perm("view_certificatekeypair", user, keypair)
|
||||
assign_perm("view_certificatekeypair_certificate", user, keypair)
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-certificate",
|
||||
kwargs={"pk": keypair.pk},
|
||||
)
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-certificate",
|
||||
@@ -202,20 +211,23 @@ class TestCrypto(APITestCase):
|
||||
),
|
||||
data={"download": True},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Content-Disposition", response)
|
||||
|
||||
def test_private_key_download(self):
|
||||
"""Test private_key export (download)"""
|
||||
self.client.force_login(create_test_admin_user())
|
||||
keypair = create_test_cert()
|
||||
user = create_test_user()
|
||||
assign_perm("view_certificatekeypair", user, keypair)
|
||||
assign_perm("view_certificatekeypair_key", user, keypair)
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-private-key",
|
||||
kwargs={"pk": keypair.pk},
|
||||
)
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-private-key",
|
||||
@@ -223,12 +235,12 @@ class TestCrypto(APITestCase):
|
||||
),
|
||||
data={"download": True},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Content-Disposition", response)
|
||||
|
||||
def test_certificate_download_denied(self):
|
||||
"""Test certificate export (download)"""
|
||||
self.client.logout()
|
||||
self.client.force_login(create_test_user())
|
||||
keypair = create_test_cert()
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
@@ -248,7 +260,7 @@ class TestCrypto(APITestCase):
|
||||
|
||||
def test_private_key_download_denied(self):
|
||||
"""Test private_key export (download)"""
|
||||
self.client.logout()
|
||||
self.client.force_login(create_test_user())
|
||||
keypair = create_test_cert()
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
@@ -284,7 +296,7 @@ class TestCrypto(APITestCase):
|
||||
kwargs={"pk": keypair.pk},
|
||||
)
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
[
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from django.http import Http404, HttpResponseBadRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.authentication import get_authorization_header
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authentication import validate_auth
|
||||
from authentik.endpoints.connectors.agent.api.agent import (
|
||||
AgentAuthenticationResponse,
|
||||
AgentTokenResponseSerializer,
|
||||
@@ -20,9 +18,8 @@ from authentik.endpoints.connectors.agent.models import (
|
||||
DeviceAuthenticationToken,
|
||||
DeviceToken,
|
||||
)
|
||||
from authentik.endpoints.models import Device
|
||||
from authentik.enterprise.endpoints.connectors.agent.auth import (
|
||||
agent_auth_fed_validate,
|
||||
DeviceAuthFedAuthentication,
|
||||
agent_auth_issue_token,
|
||||
check_device_policies,
|
||||
)
|
||||
@@ -71,23 +68,11 @@ class AgentConnectorViewSetMixin:
|
||||
detail=False,
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[],
|
||||
authentication_classes=[],
|
||||
permission_classes=[IsAuthenticated],
|
||||
authentication_classes=[DeviceAuthFedAuthentication],
|
||||
)
|
||||
def auth_fed(self, request: Request) -> Response:
|
||||
raw_token = validate_auth(get_authorization_header(request))
|
||||
if not raw_token:
|
||||
LOGGER.warning("Missing token")
|
||||
return HttpResponseBadRequest()
|
||||
device = Device.filter_not_expired(name=request.query_params.get("device")).first()
|
||||
if not device:
|
||||
LOGGER.warning("Couldn't find device")
|
||||
raise Http404
|
||||
|
||||
federated_token, connector = agent_auth_fed_validate(raw_token, device)
|
||||
LOGGER.info(
|
||||
"successfully verified JWT with provider", provider=federated_token.provider.name
|
||||
)
|
||||
federated_token, device, connector = request.auth
|
||||
|
||||
policy_result = check_device_policies(device, federated_token.user, request._request)
|
||||
if not policy_result.passing:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from django.http import Http404, HttpRequest
|
||||
from django.http import HttpRequest
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
||||
from jwt import PyJWTError, decode, encode
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authentication import get_authorization_header, validate_auth
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
@@ -41,30 +43,54 @@ def agent_auth_issue_token(device: Device, connector: AgentConnector, user: User
|
||||
return token, exp
|
||||
|
||||
|
||||
def agent_auth_fed_validate(
|
||||
raw_token: str, device: Device
|
||||
) -> tuple[AccessToken, AgentConnector | None]:
|
||||
connectors_for_device = AgentConnector.objects.filter(device__in=[device])
|
||||
connector = connectors_for_device.first()
|
||||
providers = OAuth2Provider.objects.filter(agentconnector__in=connectors_for_device)
|
||||
federated_token = AccessToken.objects.filter(token=raw_token, provider__in=providers).first()
|
||||
if not federated_token:
|
||||
LOGGER.warning("Couldn't lookup provider")
|
||||
raise Http404
|
||||
_key, _alg = federated_token.provider.jwt_key
|
||||
try:
|
||||
decode(
|
||||
raw_token,
|
||||
_key.public_key(),
|
||||
algorithms=[_alg],
|
||||
options={
|
||||
"verify_aud": False,
|
||||
},
|
||||
)
|
||||
return federated_token, connector
|
||||
except (PyJWTError, ValueError, TypeError, AttributeError) as exc:
|
||||
LOGGER.warning("failed to verify JWT", exc=exc, provider=federated_token.provider.name)
|
||||
raise ValidationError() from None
|
||||
class DeviceAuthFedAuthentication(BaseAuthentication):
|
||||
|
||||
def authenticate(self, request):
|
||||
raw_token = validate_auth(get_authorization_header(request))
|
||||
if not raw_token:
|
||||
LOGGER.warning("Missing token")
|
||||
return None
|
||||
device = Device.filter_not_expired(name=request.query_params.get("device")).first()
|
||||
if not device:
|
||||
LOGGER.warning("Couldn't find device")
|
||||
return None
|
||||
connectors_for_device = AgentConnector.objects.filter(device__in=[device])
|
||||
connector = connectors_for_device.first()
|
||||
providers = OAuth2Provider.objects.filter(agentconnector__in=connectors_for_device)
|
||||
federated_token = AccessToken.objects.filter(
|
||||
token=raw_token, provider__in=providers
|
||||
).first()
|
||||
if not federated_token:
|
||||
LOGGER.warning("Couldn't lookup provider")
|
||||
return None
|
||||
_key, _alg = federated_token.provider.jwt_key
|
||||
try:
|
||||
decode(
|
||||
raw_token,
|
||||
_key.public_key(),
|
||||
algorithms=[_alg],
|
||||
options={
|
||||
"verify_aud": False,
|
||||
},
|
||||
)
|
||||
LOGGER.info(
|
||||
"successfully verified JWT with provider", provider=federated_token.provider.name
|
||||
)
|
||||
return (federated_token.user, (federated_token, device, connector))
|
||||
except (PyJWTError, ValueError, TypeError, AttributeError) as exc:
|
||||
LOGGER.warning("failed to verify JWT", exc=exc, provider=federated_token.provider.name)
|
||||
return None
|
||||
|
||||
|
||||
class DeviceFederationAuthSchema(OpenApiAuthenticationExtension):
|
||||
"""Auth schema"""
|
||||
|
||||
target_class = DeviceAuthFedAuthentication
|
||||
name = "device_federation"
|
||||
|
||||
def get_security_definition(self, auto_schema):
|
||||
"""Auth schema"""
|
||||
return {"type": "http", "scheme": "bearer"}
|
||||
|
||||
|
||||
def check_device_policies(device: Device, user: User, request: HttpRequest):
|
||||
|
||||
@@ -98,16 +98,16 @@ class TestConnectorAuthFed(APITestCase):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}foo",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
# No device
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}foo",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_token}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
# invalid token
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agentconnector-auth-fed") + f"?device={self.device.name}",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_token}aa",
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from binascii import hexlify
|
||||
from enum import IntFlag, auto
|
||||
from urllib.parse import unquote_plus
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
@@ -17,7 +18,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.models import CertificateKeyPair, fingerprint_sha256
|
||||
from authentik.crypto.models import CertificateKeyPair, fingerprint_sha256, format_cert
|
||||
from authentik.endpoints.models import StageMode
|
||||
from authentik.enterprise.stages.mtls.models import (
|
||||
CertAttributes,
|
||||
@@ -43,14 +44,28 @@ HEADER_OUTPOST_FORWARDED = "X-Authentik-Outpost-Certificate"
|
||||
PLAN_CONTEXT_CERTIFICATE = "certificate"
|
||||
|
||||
|
||||
class ParseOptions(IntFlag):
|
||||
|
||||
# URL unquote the string
|
||||
UNQUOTE = auto()
|
||||
# Re-add PEM Header & footer, and chunk it into 64 character lines
|
||||
FORMAT = auto()
|
||||
|
||||
|
||||
class MTLSStageView(ChallengeStageView):
|
||||
|
||||
def __parse_single_cert(self, raw: str | None) -> list[Certificate]:
|
||||
def __parse_single_cert(self, raw: str | None, *options: ParseOptions) -> list[Certificate]:
|
||||
"""Helper to parse a single certificate"""
|
||||
if not raw:
|
||||
return []
|
||||
for opt in options:
|
||||
match opt:
|
||||
case ParseOptions.FORMAT:
|
||||
raw = format_cert(raw)
|
||||
case ParseOptions.UNQUOTE:
|
||||
raw = unquote_plus(raw)
|
||||
try:
|
||||
cert = load_pem_x509_certificate(unquote_plus(raw).encode())
|
||||
cert = load_pem_x509_certificate(raw.encode())
|
||||
return [cert]
|
||||
except ValueError as exc:
|
||||
self.logger.info("Failed to parse certificate", exc=exc)
|
||||
@@ -59,6 +74,7 @@ class MTLSStageView(ChallengeStageView):
|
||||
def _parse_cert_xfcc(self) -> list[Certificate]:
|
||||
"""Parse certificates in the format given to us in
|
||||
the format of the authentik router/envoy"""
|
||||
# https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert
|
||||
xfcc_raw = self.request.headers.get(HEADER_PROXY_FORWARDED)
|
||||
if not xfcc_raw:
|
||||
return []
|
||||
@@ -68,18 +84,26 @@ class MTLSStageView(ChallengeStageView):
|
||||
raw_cert = {k.split("=")[0]: k.split("=")[1] for k in el}
|
||||
if "Cert" not in raw_cert:
|
||||
continue
|
||||
certs.extend(self.__parse_single_cert(raw_cert["Cert"]))
|
||||
certs.extend(self.__parse_single_cert(raw_cert["Cert"], ParseOptions.UNQUOTE))
|
||||
return certs
|
||||
|
||||
def _parse_cert_nginx(self) -> list[Certificate]:
|
||||
"""Parse certificates in the format nginx-ingress gives to us"""
|
||||
# https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#client-certificate-authentication
|
||||
# https://github.com/kubernetes/ingress-nginx/blob/78f593b24494a0674b362faf551079f06d71b5a9/rootfs/etc/nginx/template/nginx.tmpl#L1096
|
||||
sslcc_raw = self.request.headers.get(HEADER_NGINX_FORWARDED)
|
||||
return self.__parse_single_cert(sslcc_raw)
|
||||
return self.__parse_single_cert(sslcc_raw, ParseOptions.UNQUOTE)
|
||||
|
||||
def _parse_cert_traefik(self) -> list[Certificate]:
|
||||
"""Parse certificates in the format traefik gives to us"""
|
||||
# https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/passtlsclientcert/
|
||||
ftcc_raw = self.request.headers.get(HEADER_TRAEFIK_FORWARDED)
|
||||
return self.__parse_single_cert(ftcc_raw)
|
||||
if not ftcc_raw:
|
||||
return []
|
||||
certs = []
|
||||
for cert in ftcc_raw.split(","):
|
||||
certs.extend(self.__parse_single_cert(cert, ParseOptions.UNQUOTE, ParseOptions.FORMAT))
|
||||
return certs
|
||||
|
||||
def _parse_cert_outpost(self) -> list[Certificate]:
|
||||
"""Parse certificates in the format outposts give to us. Also authenticates
|
||||
@@ -92,7 +116,7 @@ class MTLSStageView(ChallengeStageView):
|
||||
) and not user.has_perm("authentik_stages_mtls.pass_outpost_certificate"):
|
||||
return []
|
||||
outpost_raw = self.request.headers.get(HEADER_OUTPOST_FORWARDED)
|
||||
return self.__parse_single_cert(outpost_raw)
|
||||
return self.__parse_single_cert(outpost_raw, ParseOptions.UNQUOTE)
|
||||
|
||||
def get_authorities(self) -> list[CertificateKeyPair] | None:
|
||||
# We can't access `certificate_authorities` on `self.executor.current_stage`, as that would
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from ssl import PEM_FOOTER, PEM_HEADER
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
@@ -51,6 +52,10 @@ class MTLSStageTests(FlowTestCase):
|
||||
User.objects.filter(username="client").delete()
|
||||
self.cert_user = create_test_user(username="client")
|
||||
|
||||
def _format_traefik(self, cert: str | None = None):
|
||||
cert = cert if cert else self.client_cert
|
||||
return quote_plus(cert.replace(PEM_HEADER, "").replace(PEM_FOOTER, "").replace("\n", ""))
|
||||
|
||||
def test_parse_xfcc(self):
|
||||
"""Test authentik Proxy/Envoy's XFCC format"""
|
||||
with self.assertFlowFinishes() as plan:
|
||||
@@ -78,7 +83,7 @@ class MTLSStageTests(FlowTestCase):
|
||||
with self.assertFlowFinishes() as plan:
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
headers={"X-Forwarded-TLS-Client-Cert": quote_plus(self.client_cert)},
|
||||
headers={"X-Forwarded-TLS-Client-Cert": self._format_traefik()},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageRedirects(res, reverse("authentik_core:root-redirect"))
|
||||
@@ -138,7 +143,9 @@ class MTLSStageTests(FlowTestCase):
|
||||
with self.assertFlowFinishes() as plan:
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
headers={"X-Forwarded-TLS-Client-Cert": quote_plus(cert.certificate_data)},
|
||||
headers={
|
||||
"X-Forwarded-TLS-Client-Cert": self._format_traefik(cert.certificate_data)
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageResponse(res, self.flow, component="ak-stage-access-denied")
|
||||
@@ -149,7 +156,7 @@ class MTLSStageTests(FlowTestCase):
|
||||
User.objects.filter(username="client").delete()
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
headers={"X-Forwarded-TLS-Client-Cert": quote_plus(self.client_cert)},
|
||||
headers={"X-Forwarded-TLS-Client-Cert": self._format_traefik()},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageResponse(res, self.flow, component="ak-stage-access-denied")
|
||||
@@ -163,7 +170,7 @@ class MTLSStageTests(FlowTestCase):
|
||||
with self.assertFlowFinishes() as plan:
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
headers={"X-Forwarded-TLS-Client-Cert": quote_plus(self.client_cert)},
|
||||
headers={"X-Forwarded-TLS-Client-Cert": self._format_traefik()},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageRedirects(res, reverse("authentik_core:root-redirect"))
|
||||
@@ -176,7 +183,7 @@ class MTLSStageTests(FlowTestCase):
|
||||
self.stage.save()
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
headers={"X-Forwarded-TLS-Client-Cert": quote_plus(self.client_cert)},
|
||||
headers={"X-Forwarded-TLS-Client-Cert": self._format_traefik()},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageRedirects(res, reverse("authentik_core:root-redirect"))
|
||||
@@ -187,7 +194,7 @@ class MTLSStageTests(FlowTestCase):
|
||||
self.stage.save()
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
headers={"X-Forwarded-TLS-Client-Cert": quote_plus(self.client_cert)},
|
||||
headers={"X-Forwarded-TLS-Client-Cert": self._format_traefik()},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageResponse(res, self.flow, component="ak-stage-access-denied")
|
||||
@@ -209,7 +216,7 @@ class MTLSStageTests(FlowTestCase):
|
||||
with self.assertFlowFinishes() as plan:
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
headers={"X-Forwarded-TLS-Client-Cert": quote_plus(self.client_cert)},
|
||||
headers={"X-Forwarded-TLS-Client-Cert": self._format_traefik()},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageRedirects(res, reverse("authentik_core:root-redirect"))
|
||||
|
||||
@@ -4,6 +4,7 @@ from prometheus_client import Gauge, Histogram
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
from authentik.tenants.flags import Flag
|
||||
|
||||
GAUGE_FLOWS_CACHED = Gauge(
|
||||
"authentik_flows_cached",
|
||||
@@ -22,6 +23,12 @@ HIST_FLOWS_PLAN_TIME = Histogram(
|
||||
)
|
||||
|
||||
|
||||
class RefreshOtherFlowsAfterAuthentication(Flag[bool], key="flows_refresh_others"):
|
||||
|
||||
default = False
|
||||
visibility = "public"
|
||||
|
||||
|
||||
class AuthentikFlowsConfig(ManagedAppConfig):
|
||||
"""authentik flows app config"""
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""authentik stage Base view"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
@@ -164,6 +165,16 @@ class ChallengeStageView(StageView):
|
||||
self.logger.warning("failed to template title", exc=exc)
|
||||
return self.executor.flow.title
|
||||
|
||||
@property
|
||||
def cancel_url(self) -> str:
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||
|
||||
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(NEXT_ARG_NAME)
|
||||
url = reverse("authentik_flows:cancel")
|
||||
if next_param:
|
||||
return f"{url}?{urlencode({NEXT_ARG_NAME: next_param})}"
|
||||
return url
|
||||
|
||||
def _get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
with (
|
||||
start_span(
|
||||
@@ -186,7 +197,7 @@ class ChallengeStageView(StageView):
|
||||
data={
|
||||
"title": self.format_title(),
|
||||
"background": self.executor.flow.background_url(self.request),
|
||||
"cancel_url": reverse("authentik_flows:cancel"),
|
||||
"cancel_url": self.cancel_url,
|
||||
"layout": self.executor.flow.layout,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -32,8 +32,10 @@ class FlowTestCase(APITestCase):
|
||||
self.assertIsNotNone(raw_response["component"])
|
||||
if flow:
|
||||
self.assertIn("flow_info", raw_response)
|
||||
self.assertEqual(
|
||||
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
|
||||
self.assertTrue(
|
||||
raw_response["flow_info"]["cancel_url"].startswith(
|
||||
reverse("authentik_flows:cancel")
|
||||
)
|
||||
)
|
||||
# We don't check the flow title since it will most likely go
|
||||
# through ChallengeStageView.format_title() so might not match 1:1
|
||||
|
||||
@@ -36,6 +36,7 @@ from authentik.policies.types import PolicyResult
|
||||
from authentik.stages.deny.models import DenyStage
|
||||
from authentik.stages.dummy.models import DummyStage
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
|
||||
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False, "foo"))
|
||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||
@@ -692,3 +693,49 @@ class TestFlowExecutor(FlowTestCase):
|
||||
self.client.logout()
|
||||
response = self.client.post(url, data="{", content_type="application/json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_cancel_next(self):
|
||||
"""Test cancel URL with ?next param set"""
|
||||
flow = create_test_flow()
|
||||
|
||||
# Stage 0 is an identification stage
|
||||
ident_stage = IdentificationStage.objects.create(
|
||||
name=generate_id(),
|
||||
user_fields=[UserFields.USERNAME],
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow,
|
||||
stage=ident_stage,
|
||||
order=0,
|
||||
)
|
||||
|
||||
# Stage 1 is a password stage
|
||||
password_stage = PasswordStage.objects.create(name=generate_id(), backends=[])
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow,
|
||||
stage=password_stage,
|
||||
order=1,
|
||||
)
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
|
||||
+ f"?{urlencode({QS_QUERY: urlencode({NEXT_ARG_NAME: "/foo"})})}"
|
||||
)
|
||||
self.assertStageResponse(res, flow, component="ak-stage-identification")
|
||||
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
|
||||
+ f"?{urlencode({QS_QUERY: urlencode({NEXT_ARG_NAME: "/foo"})})}",
|
||||
data={"component": "ak-stage-identification", "uid_field": generate_id()},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
res,
|
||||
flow,
|
||||
flow_info={
|
||||
"background": "/static/dist/assets/images/flow_background.jpg",
|
||||
"cancel_url": "/flows/-/cancel/?next=%2Ffoo",
|
||||
"layout": "stacked",
|
||||
"title": flow.title,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -479,6 +479,9 @@ class CancelView(View):
|
||||
if SESSION_KEY_PLAN in request.session:
|
||||
del request.session[SESSION_KEY_PLAN]
|
||||
LOGGER.debug("Canceled current plan")
|
||||
next_url = self.request.GET.get(NEXT_ARG_NAME)
|
||||
if next_url and not is_url_absolute(next_url):
|
||||
return redirect(next_url)
|
||||
return redirect("authentik_flows:default-invalidation")
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.fields import BooleanField, ListField, SerializerMethodField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
@@ -70,7 +71,7 @@ class FlowInspectorView(APIView):
|
||||
|
||||
flow: Flow
|
||||
_logger: BoundLogger
|
||||
permission_classes = []
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def setup(self, request: HttpRequest, flow_slug: str):
|
||||
super().setup(request, flow_slug=flow_slug)
|
||||
|
||||
@@ -5,7 +5,7 @@ from channels.exceptions import DenyConnection
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authentication import bearer_auth
|
||||
from authentik.api.authentication import TokenAuthentication
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -32,12 +32,12 @@ class TokenOutpostMiddleware:
|
||||
raw_header = headers[b"authorization"]
|
||||
|
||||
try:
|
||||
user = bearer_auth(raw_header)
|
||||
user_ctx = TokenAuthentication().bearer_auth(raw_header)
|
||||
# user is only None when no header was given, in which case we deny too
|
||||
if not user:
|
||||
if not user_ctx:
|
||||
raise DenyConnection()
|
||||
user, _ = user_ctx
|
||||
scope["user"] = user
|
||||
except AuthenticationFailed as exc:
|
||||
LOGGER.warning("Failed to authenticate", exc=exc)
|
||||
raise DenyConnection() from None
|
||||
|
||||
scope["user"] = user
|
||||
|
||||
@@ -9,10 +9,9 @@ from defusedxml.lxml import fromstring
|
||||
from lxml import etree # nosec
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.crypto.models import CertificateKeyPair, format_cert
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.utils.encoding import PEM_FOOTER, PEM_HEADER
|
||||
from authentik.sources.saml.models import SAMLNameIDPolicy
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
NS_MAP,
|
||||
@@ -24,18 +23,6 @@ from authentik.sources.saml.processors.constants import (
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def format_pem_certificate(unformatted_cert: str) -> str:
|
||||
"""Format single, inline certificate into PEM Format"""
|
||||
# Ensure that all linebreaks are gone
|
||||
unformatted_cert = unformatted_cert.replace("\n", "")
|
||||
chunks, chunk_size = len(unformatted_cert), 64
|
||||
lines = [PEM_HEADER]
|
||||
for i in range(0, chunks, chunk_size):
|
||||
lines.append(unformatted_cert[i : i + chunk_size])
|
||||
lines.append(PEM_FOOTER)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ServiceProviderMetadata:
|
||||
"""SP Metadata Dataclass"""
|
||||
@@ -87,7 +74,7 @@ class ServiceProviderMetadataParser:
|
||||
)
|
||||
if len(signing_certs) < 1:
|
||||
return None
|
||||
raw_cert = format_pem_certificate(signing_certs[0])
|
||||
raw_cert = format_cert(signing_certs[0])
|
||||
# sanity check, make sure the certificate is valid.
|
||||
load_pem_x509_certificate(raw_cert.encode("utf-8"), default_backend())
|
||||
return CertificateKeyPair(
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
import base64
|
||||
import zlib
|
||||
|
||||
PEM_HEADER = "-----BEGIN CERTIFICATE-----"
|
||||
PEM_FOOTER = "-----END CERTIFICATE-----"
|
||||
from ssl import PEM_FOOTER, PEM_HEADER
|
||||
|
||||
|
||||
def decode_base64_and_inflate(encoded: str, encoding="utf-8") -> str:
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"""websocket Message consumer"""
|
||||
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from django.core.cache import cache
|
||||
|
||||
from authentik.root.messages.storage import CACHE_PREFIX
|
||||
|
||||
|
||||
class MessageConsumer(JsonWebsocketConsumer):
|
||||
"""Consumer which sends django.contrib.messages Messages over WS.
|
||||
channel_name is saved into cache with user_id, and when a add_message is called"""
|
||||
|
||||
session_key: str
|
||||
|
||||
def connect(self):
|
||||
self.accept()
|
||||
self.session_key = self.scope["session"].session_key
|
||||
if not self.session_key:
|
||||
return
|
||||
cache.set(f"{CACHE_PREFIX}{self.session_key}_messages_{self.channel_name}", True, None)
|
||||
|
||||
def disconnect(self, code):
|
||||
cache.delete(f"{CACHE_PREFIX}{self.session_key}_messages_{self.channel_name}")
|
||||
|
||||
def event_update(self, event: dict):
|
||||
"""Event handler which is called by Messages Storage backend"""
|
||||
self.send_json(event)
|
||||
@@ -254,7 +254,7 @@ SESSION_COOKIE_AGE = timedelta_from_string(
|
||||
).total_seconds()
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
|
||||
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
|
||||
MESSAGE_STORAGE = "authentik.root.ws.storage.ChannelsStorage"
|
||||
|
||||
MIDDLEWARE_FIRST = [
|
||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||
|
||||
58
authentik/root/ws/consumer.py
Normal file
58
authentik/root/ws/consumer.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""websocket Message consumer"""
|
||||
|
||||
from hashlib import sha256
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from django.core.cache import cache
|
||||
from django.db import connection
|
||||
|
||||
from authentik.root.ws.storage import CACHE_PREFIX
|
||||
|
||||
|
||||
def build_session_group(session_key: str):
|
||||
return sha256(
|
||||
f"{connection.schema_name}/group_client_session_{str(session_key)}".encode()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def build_device_group(session_key: str):
|
||||
return sha256(
|
||||
f"{connection.schema_name}/group_client_device_{str(session_key)}".encode()
|
||||
).hexdigest()
|
||||
|
||||
|
||||
class MessageConsumer(JsonWebsocketConsumer):
|
||||
"""Consumer which sends django.contrib.messages Messages over WS.
|
||||
channel_name is saved into cache with user_id, and when a add_message is called"""
|
||||
|
||||
session_key: str
|
||||
device_cookie: str | None = None
|
||||
|
||||
def connect(self):
|
||||
self.accept()
|
||||
self.session_key = self.scope["session"].session_key
|
||||
if self.session_key:
|
||||
cache.set(f"{CACHE_PREFIX}{self.session_key}_messages_{self.channel_name}", True, None)
|
||||
if device_cookie := self.scope["cookies"].get("authentik_device", None):
|
||||
self.device_cookie = device_cookie
|
||||
async_to_sync(self.channel_layer.group_add)(
|
||||
build_device_group(self.device_cookie), self.channel_name
|
||||
)
|
||||
|
||||
def disconnect(self, code):
|
||||
if self.session_key:
|
||||
cache.delete(f"{CACHE_PREFIX}{self.session_key}_messages_{self.channel_name}")
|
||||
if self.device_cookie:
|
||||
print("removing from group", build_session_group(self.session_key))
|
||||
async_to_sync(self.channel_layer.group_discard)(
|
||||
build_device_group(self.device_cookie), self.channel_name
|
||||
)
|
||||
|
||||
def event_message(self, event: dict):
|
||||
"""Event handler which is called by Messages Storage backend"""
|
||||
self.send_json(event)
|
||||
|
||||
def event_session_authenticated(self, event: dict):
|
||||
"""Event handler post user authentication"""
|
||||
self.send_json({"message_type": "session.authenticated"})
|
||||
@@ -31,7 +31,7 @@ class ChannelsStorage(SessionStorage):
|
||||
async_to_sync(self.channel.send)(
|
||||
uid,
|
||||
{
|
||||
"type": "event.update",
|
||||
"type": "event.message",
|
||||
"message_type": "message",
|
||||
"level": message.level_tag,
|
||||
"tags": message.tags,
|
||||
@@ -9,6 +9,7 @@ from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, ChoiceField, IntegerField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
@@ -83,7 +84,7 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
|
||||
),
|
||||
},
|
||||
)
|
||||
@action(methods=["POST"], detail=True, permission_classes=[])
|
||||
@action(methods=["POST"], detail=True, permission_classes=[IsAuthenticated])
|
||||
def enrollment_status(self, request: Request, pk: str) -> Response:
|
||||
"""Check enrollment status of user details in current session"""
|
||||
stage: AuthenticatorDuoStage = AuthenticatorDuoStage.objects.filter(pk=pk).first()
|
||||
@@ -97,7 +98,7 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
|
||||
return Response({"duo_response": status})
|
||||
|
||||
@permission_required(
|
||||
"", ["authentik_stages_authenticator_duo.add_duodevice", "authentik_core.view_user"]
|
||||
None, ["authentik_stages_authenticator_duo.add_duodevice", "authentik_core.view_user"]
|
||||
)
|
||||
@extend_schema(
|
||||
request=AuthenticatorDuoStageManualDeviceImport(),
|
||||
|
||||
@@ -40,6 +40,7 @@ class AuthenticatorDuoStageTests(FlowTestCase):
|
||||
|
||||
def test_api_enrollment_invalid(self):
|
||||
"""Test `enrollment_status`"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:authenticatorduostage-enrollment-status",
|
||||
@@ -52,6 +53,7 @@ class AuthenticatorDuoStageTests(FlowTestCase):
|
||||
|
||||
def test_api_enrollment(self):
|
||||
"""Test `enrollment_status`"""
|
||||
self.client.force_login(self.user)
|
||||
stage = AuthenticatorDuoStage.objects.create(
|
||||
name=generate_id(),
|
||||
client_id=generate_id(),
|
||||
|
||||
@@ -21,6 +21,7 @@ from rest_framework.mixins import (
|
||||
ListModelMixin,
|
||||
RetrieveModelMixin,
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.utils.serializer_helpers import ReturnList
|
||||
@@ -143,7 +144,7 @@ class TaskViewSet(
|
||||
.filter(tenant=get_current_tenant())
|
||||
)
|
||||
|
||||
@permission_required(None, ["authentik_tasks.retry_task"])
|
||||
@permission_required("authentik_tasks.retry_task")
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses={
|
||||
@@ -152,7 +153,7 @@ class TaskViewSet(
|
||||
404: OpenApiResponse(description="Task not found"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["POST"], permission_classes=[])
|
||||
@action(detail=True, methods=["POST"], permission_classes=[IsAuthenticated])
|
||||
def retry(self, request: Request, pk=None) -> Response:
|
||||
"""Retry task"""
|
||||
task: Task = self.get_object()
|
||||
@@ -162,6 +163,7 @@ class TaskViewSet(
|
||||
broker.enqueue(Message.decode(task.message))
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required(None, ["authentik_tasks.view_task"])
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses={
|
||||
@@ -182,7 +184,7 @@ class TaskViewSet(
|
||||
),
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["GET"], permission_classes=[])
|
||||
@action(detail=False, methods=["GET"], permission_classes=[IsAuthenticated])
|
||||
def status(self, request: Request) -> Response:
|
||||
"""Global status summary for all tasks"""
|
||||
response = {}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
from django.apps import apps
|
||||
from django.http import HttpResponseNotFound
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.tenants.api.tenants import TenantApiKeyPermission
|
||||
from authentik.tenants.api.tenants import TenantApiKeyAuthentication
|
||||
from authentik.tenants.models import Domain
|
||||
|
||||
|
||||
@@ -29,8 +30,8 @@ class DomainViewSet(ModelViewSet):
|
||||
"tenant__schema_name",
|
||||
]
|
||||
ordering = ["domain"]
|
||||
authentication_classes = []
|
||||
permission_classes = [TenantApiKeyPermission]
|
||||
authentication_classes = [TenantApiKeyAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [OrderingFilter, SearchFilter]
|
||||
filterset_fields = []
|
||||
|
||||
|
||||
@@ -8,18 +8,17 @@ from django.http import HttpResponseNotFound
|
||||
from django.http.request import urljoin
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.authentication import get_authorization_header
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import BasePermission
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import DateTimeField
|
||||
from rest_framework.views import View
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.authentication import validate_auth
|
||||
from authentik.api.authentication import IPCUser, validate_auth
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.config import CONFIG
|
||||
@@ -27,17 +26,19 @@ from authentik.recovery.lib import create_admin_group, create_recovery_token
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class TenantApiKeyPermission(BasePermission):
|
||||
class TenantApiKeyAuthentication(BaseAuthentication):
|
||||
"""Authentication based on tenants.api_key"""
|
||||
|
||||
def has_permission(self, request: Request, view: View) -> bool:
|
||||
def authenticate(self, request: Request) -> bool:
|
||||
key = CONFIG.get("tenants.api_key", "")
|
||||
if not key:
|
||||
return False
|
||||
return None
|
||||
token = validate_auth(get_authorization_header(request))
|
||||
if token is None:
|
||||
return False
|
||||
return compare_digest(token, key)
|
||||
return None
|
||||
if not compare_digest(token, key):
|
||||
return None
|
||||
return (IPCUser(), None)
|
||||
|
||||
|
||||
class TenantSerializer(ModelSerializer):
|
||||
@@ -84,8 +85,8 @@ class TenantViewSet(ModelViewSet):
|
||||
"domains__domain",
|
||||
]
|
||||
ordering = ["schema_name"]
|
||||
authentication_classes = []
|
||||
permission_classes = [TenantApiKeyPermission]
|
||||
authentication_classes = [TenantApiKeyAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [OrderingFilter, SearchFilter]
|
||||
filterset_fields = []
|
||||
|
||||
|
||||
@@ -5280,6 +5280,8 @@
|
||||
"authentik_crypto.change_certificatekeypair",
|
||||
"authentik_crypto.delete_certificatekeypair",
|
||||
"authentik_crypto.view_certificatekeypair",
|
||||
"authentik_crypto.view_certificatekeypair_certificate",
|
||||
"authentik_crypto.view_certificatekeypair_key",
|
||||
"authentik_endpoints.add_connector",
|
||||
"authentik_endpoints.add_device",
|
||||
"authentik_endpoints.add_deviceaccessgroup",
|
||||
@@ -5953,7 +5955,9 @@
|
||||
"add_certificatekeypair",
|
||||
"change_certificatekeypair",
|
||||
"delete_certificatekeypair",
|
||||
"view_certificatekeypair"
|
||||
"view_certificatekeypair",
|
||||
"view_certificatekeypair_certificate",
|
||||
"view_certificatekeypair_key"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
@@ -10604,6 +10608,8 @@
|
||||
"authentik_crypto.change_certificatekeypair",
|
||||
"authentik_crypto.delete_certificatekeypair",
|
||||
"authentik_crypto.view_certificatekeypair",
|
||||
"authentik_crypto.view_certificatekeypair_certificate",
|
||||
"authentik_crypto.view_certificatekeypair_key",
|
||||
"authentik_endpoints.add_connector",
|
||||
"authentik_endpoints.add_device",
|
||||
"authentik_endpoints.add_deviceaccessgroup",
|
||||
|
||||
4
go.mod
4
go.mod
@@ -29,10 +29,10 @@ require (
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/sethvargo/go-envconfig v1.3.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025120.15
|
||||
goauthentik.io/api/v3 v3.2025120.16
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
golang.org/x/sync v0.18.0
|
||||
|
||||
8
go.sum
8
go.sum
@@ -180,8 +180,8 @@ github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -214,8 +214,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
goauthentik.io/api/v3 v3.2025120.15 h1:9/Cm1F1Ei1tNCp8vR2n+dTkiWcXx25Aixo/vKp9MMm4=
|
||||
goauthentik.io/api/v3 v3.2025120.15/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
goauthentik.io/api/v3 v3.2025120.16 h1:tFXfIUp2p15jBVJSIAxmCS2v1Cu/BzjFhzbF+ore8ug=
|
||||
goauthentik.io/api/v3 v3.2025120.16/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
|
||||
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/ldap ./cmd/ldap
|
||||
|
||||
# Stage 2: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:cf233be62def2486ae664264040195201b3c76e2bdb24c1a4c86e024fe27d9b0
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:a80dbbd0ca1f7f20181984960e2a0618fdf9a6e1d90635bb6e034eedee185eb5
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-01 17:05+0000\n"
|
||||
"POT-Creation-Date: 2025-12-05 00:11+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"
|
||||
@@ -17,6 +17,47 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: authentik/admin/files/api.py
|
||||
#, python-brace-format
|
||||
msgid "File size ({file.size}B) exceeds maximum allowed "
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/files/validation.py
|
||||
msgid "File name cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/files/validation.py
|
||||
msgid ""
|
||||
"File name can only contain letters (a-z, A-Z), numbers (0-9), dots (.), "
|
||||
"hyphens (-), underscores (_), and forward slashes (/)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/files/validation.py
|
||||
msgid "File name cannot contain duplicate /"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/files/validation.py
|
||||
msgid "Absolute paths are not allowed"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/files/validation.py
|
||||
msgid "Parent directory references ('..') are not allowed"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/files/validation.py
|
||||
msgid "Paths cannot start with '.'"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/files/validation.py
|
||||
#, python-brace-format
|
||||
msgid "File name too long (max {MAX_FILE_NAME_LENGTH} characters)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/files/validation.py
|
||||
#, python-brace-format
|
||||
msgid "Path component too long (max {MAX_PATH_COMPONENT_LENGTH} characters)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/models.py
|
||||
msgid "Version history"
|
||||
msgstr ""
|
||||
@@ -521,6 +562,14 @@ msgstr ""
|
||||
msgid "Certificate-Key Pairs"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "View Certificate-Key pair's certificate"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "View Certificate-Key pair's private key"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/tasks.py
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr ""
|
||||
@@ -619,6 +668,14 @@ msgstr ""
|
||||
msgid "Device access groups"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Endpoint Stage"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/models.py
|
||||
msgid "Endpoint Stages"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/endpoints/tasks.py
|
||||
msgid "Sync endpoints."
|
||||
msgstr ""
|
||||
@@ -2415,6 +2472,11 @@ msgstr ""
|
||||
msgid "Alter authentik behavior for vendor-specific SCIM implementations."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid ""
|
||||
"Cache duration for ServiceProviderConfig responses. Set minutes=0 to disable."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Provider"
|
||||
msgstr ""
|
||||
@@ -2499,6 +2561,14 @@ msgstr ""
|
||||
msgid "Can edit system settings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can view media files"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can manage media files"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/recovery/management/commands/create_admin_group.py
|
||||
msgid "Create admin group if the default group gets deleted."
|
||||
msgstr ""
|
||||
@@ -2905,6 +2975,14 @@ msgstr ""
|
||||
msgid "Discord OAuth Sources"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Slack OAuth Source"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Slack OAuth Sources"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Patreon OAuth Source"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -47,7 +47,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/proxy ./cmd/proxy
|
||||
|
||||
# Stage 3: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:cf233be62def2486ae664264040195201b3c76e2bdb24c1a4c86e024fe27d9b0
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:a80dbbd0ca1f7f20181984960e2a0618fdf9a6e1d90635bb6e034eedee185eb5
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
|
||||
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/radius ./cmd/radius
|
||||
|
||||
# Stage 2: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:cf233be62def2486ae664264040195201b3c76e2bdb24c1a4c86e024fe27d9b0
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:a80dbbd0ca1f7f20181984960e2a0618fdf9a6e1d90635bb6e034eedee185eb5
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
|
||||
12
schema.yml
12
schema.yml
@@ -35696,7 +35696,10 @@ components:
|
||||
properties:
|
||||
policies_buffered_access_view:
|
||||
type: boolean
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
required:
|
||||
- flows_refresh_others
|
||||
- policies_buffered_access_view
|
||||
readOnly: true
|
||||
required:
|
||||
@@ -48756,7 +48759,10 @@ components:
|
||||
properties:
|
||||
policies_buffered_access_view:
|
||||
type: boolean
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
required:
|
||||
- flows_refresh_others
|
||||
- policies_buffered_access_view
|
||||
PatchedSourceStageRequest:
|
||||
type: object
|
||||
@@ -53189,7 +53195,10 @@ components:
|
||||
properties:
|
||||
policies_buffered_access_view:
|
||||
type: boolean
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
required:
|
||||
- flows_refresh_others
|
||||
- policies_buffered_access_view
|
||||
required:
|
||||
- flags
|
||||
@@ -53251,7 +53260,10 @@ components:
|
||||
properties:
|
||||
policies_buffered_access_view:
|
||||
type: boolean
|
||||
flows_refresh_others:
|
||||
type: boolean
|
||||
required:
|
||||
- flows_refresh_others
|
||||
- policies_buffered_access_view
|
||||
required:
|
||||
- flags
|
||||
|
||||
206
uv.lock
generated
206
uv.lock
generated
@@ -97,15 +97,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.11.0"
|
||||
version = "4.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -143,11 +142,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.10.0"
|
||||
version = "3.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483, upload-time = "2025-10-05T09:15:06.557Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050, upload-time = "2025-10-05T09:15:05.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -383,19 +382,24 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "autobahn"
|
||||
version = "25.10.2"
|
||||
version = "25.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cbor2" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "hyperlink" },
|
||||
{ name = "msgpack", marker = "platform_python_implementation == 'CPython'" },
|
||||
{ name = "py-ubjson" },
|
||||
{ name = "txaio" },
|
||||
{ name = "u-msgpack-python", marker = "platform_python_implementation != 'CPython'" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/5d/095541ec46347cdb6d94b1cde7b0236eee7dcdaadb2daad45232d74eeff1/autobahn-25.10.2.tar.gz", hash = "sha256:173d5d836789dffc4292473ea359dcb7f708456a0ff82dcb8ca938d6ccadb12f", size = 375689, upload-time = "2025-10-22T23:34:56.017Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/3a/aab5632fbd88ed26d330ab156af80c9a90419dfa2841296fd556a65e5fac/autobahn-25.11.1.tar.gz", hash = "sha256:52e62b9cc80c3e989b182952a60fd25c9a69afb00854a925a2b185f7b1f73cf1", size = 447019, upload-time = "2025-11-24T08:31:27.919Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/f6/601e87ab8e314e211850570325acc59dad27fc78268b919a8b8344e61785/autobahn-25.10.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:7b503e282082b823e85e963d00b49ed72aeae79d4d3b3929a997cad0780ffdb1", size = 517000, upload-time = "2025-10-22T23:34:40.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/d1/e8f42bbbcbac63ff7041322523479de78138cf527ff43a814b363dc9609a/autobahn-25.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_34_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:764f01bf3778a4e3fd1f6d3df9bfbf079772181c5e4915af5a588f7aa92d3d96", size = 569847, upload-time = "2025-10-22T23:34:41.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/04/8cd5eac3be9f5e61d8d7299f0813da0d420eb49964d1884394fa67f7d84f/autobahn-25.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a66252583ee2b00e55d4f311618f87e7f0c0d1f837bbeb40b9277fc86ea027fd", size = 545484, upload-time = "2025-10-22T23:34:42.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c5/1512d54744a4565e2c3e1a16782d72241ed8a3800b12b9e097b5d4041d8d/autobahn-25.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:684fe4200ede3840b4973da8c04f0026459c8189d6bc10df31b1d933b5f01b7e", size = 525963, upload-time = "2025-10-22T23:34:43.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9e/ff60f0c88729811ca66bea4f293e03460e0a28870cc2721e47792c184fb0/autobahn-25.11.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:78910e9d8393198e7c87d072a4d22c52110422294aff90aa9ab5c253e336312f", size = 615347, upload-time = "2025-11-24T08:31:10.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/bc/997418f7c9a29ddee8d5a8391da9cba1aa49fffdbb4332702b7fee3b757c/autobahn-25.11.1-cp313-cp313-manylinux1_x86_64.manylinux_2_34_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:966bfd82669005c2b12872dfd556ac76e23d73438dc9193136b89263511d9245", size = 668946, upload-time = "2025-11-24T08:31:11.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/d1/6d3540714e4770a18ffdd1f04f15f07df3f802044064d2465b746c0dfeba/autobahn-25.11.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d7e26d9f3bba13b2e3f5dcc422be603b913df5dfaef1758d78cd0fd04b36249", size = 644586, upload-time = "2025-11-24T08:31:13.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/1a/03233afd8c3196a7b2510c7f103deb8fe06b5384683477c39a1bd6e4a573/autobahn-25.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:e6171e02ed7f70436c17cdfb12fc6ac9b20cd2a9c8e3993b192d1c9d891f5cc5", size = 625171, upload-time = "2025-11-24T08:31:14.785Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -586,43 +590,43 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "blessed"
|
||||
version = "1.24.0"
|
||||
version = "1.25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinxed", marker = "sys_platform == 'win32'" },
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/43/a01951b548a8006f58a6e8f46a958f35405853305610f1f5e86578aab7c9/blessed-1.24.0.tar.gz", hash = "sha256:7822698616deb79dc8897215eef8ed56b6a3fc537dc08ffce2f2020697c6c0d4", size = 6746429, upload-time = "2025-11-16T19:17:18.486Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/cd/eed8b82f1fabcb817d84b24d0780b86600b5c3df7ec4f890bcbb2371b0ad/blessed-1.25.0.tar.gz", hash = "sha256:606aebfea69f85915c7ca6a96eb028e0031d30feccc5688e13fd5cec8277b28d", size = 6746381, upload-time = "2025-11-18T18:43:52.71Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/88/d4681b7ff72b7f8fc01ec87534fc50bfdd2103b137e5acb9493884f06ef5/blessed-1.24.0-py3-none-any.whl", hash = "sha256:177d36ce89db91c8a61e9cf2085d5cedb6f1617dcc7c39b604bb474e2e192ec7", size = 95531, upload-time = "2025-11-16T19:17:16.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/2c/e9b6dd824fb6e76dbd39a308fc6f497320afd455373aac8518ca3eba7948/blessed-1.25.0-py3-none-any.whl", hash = "sha256:e52b9f778b9e10c30b3f17f6b5f5d2208d1e9b53b270f1d94fc61a243fc4708f", size = 95646, upload-time = "2025-11-18T18:43:50.924Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.40.75"
|
||||
version = "1.42.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/2c/0a6e49612ef9868382ef76292da2dd1dee7c74a0f1ec95323e76f6e2ef4b/boto3-1.40.75.tar.gz", hash = "sha256:a5219a2f397f8616462d7908e696c281f120aa2d8458280ff24f7ddeb2108faf", size = 111629, upload-time = "2025-11-17T21:58:37.667Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/28/b2/08e0d2e0ee0a189762e9c803a7980c835d94a8c395660cc115a4a6833f49/boto3-1.42.1.tar.gz", hash = "sha256:137fbea593a30afa1b75656ea1f1ff8796be608a8c77f1b606c4473289679898", size = 112793, upload-time = "2025-12-02T17:28:29.524Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/65/85/2b0ea3ca19447d3a681b59b712a8f7861bfd0bc0129efd8a2da09d272837/boto3-1.40.75-py3-none-any.whl", hash = "sha256:c246fb35d9978b285c5b827a20b81c9e77d52f99c9d175fbd91f14396432953f", size = 139360, upload-time = "2025-11-17T21:58:36.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/04/5da253f071d9409e3b0be0c79118bbad6c99fe8bd96cb7ef500083fc8aa7/boto3-1.42.1-py3-none-any.whl", hash = "sha256:9a8f9799afff600ff5cb43f57a619a5375ea71077ec958bda70e296378da7024", size = 140619, upload-time = "2025-12-02T17:28:27.88Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.40.75"
|
||||
version = "1.42.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/11/a6a07cbe12e0161063f2dac82bb7a8f48f649b394863315cd6f3149b82ac/botocore-1.40.75.tar.gz", hash = "sha256:bf8b067209fee5a9738800d41852e113b8ebdb01bd7f1e8b4541d55ecdbdb8f3", size = 14475952, upload-time = "2025-11-17T21:58:27.24Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/b5/3ce4e1eaf86953625b98fdcf40afc40a5682a76e140baf976d5e2dc6a9cc/botocore-1.42.1.tar.gz", hash = "sha256:3337df815c69dd87c314ee29329b8ea411ad3562fb6563d139bbe085dac14ce0", size = 14839894, upload-time = "2025-12-02T17:28:19.053Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/29/15627031629f27230ee38bc7f55328b310794010c3039f0ecd353c06dc63/botocore-1.40.75-py3-none-any.whl", hash = "sha256:e822004688ca8035c518108e27d5b450d3ab0e0b3a73bcb8b87b80a8e5bd1910", size = 14141572, upload-time = "2025-11-17T21:58:23.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/a7/2e36617497b7f1af8bde00b3a737688eaa4017ea3657a0be64ef7cc0baa9/botocore-1.42.1-py3-none-any.whl", hash = "sha256:9d49f5197487f9f71daa9c5397f81484ffcc0dc1cf89a63e94ae3e5a27faa98c", size = 14513092, upload-time = "2025-12-02T17:28:15.559Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -840,14 +844,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cron-converter"
|
||||
version = "1.2.2"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dateutil" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/b9/744734ae853c43f8b71510402e0ab0106ba96a741113f9e26e89fde40736/cron_converter-1.2.2.tar.gz", hash = "sha256:b987525ddf7d5ad28286620622f00dde61c73833d1f05c332a26c389a9c512c3", size = 14509, upload-time = "2025-07-21T09:25:00.01Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/1a/b670d969f3cf6a46322f3d28f7fd6d13294d4f20a5e132beacb1ee1981b9/cron_converter-1.3.1.tar.gz", hash = "sha256:53eb26be3eb2e0f206a6e227ca0c19b3807b44a24b39f4eda3718703e6474f4a", size = 15738, upload-time = "2025-12-02T19:03:33.633Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/f8/1698a37dd13fc120a96a062c10dba11cade6ebb71b68a14ee177032b4b61/cron_converter-1.2.2-py3-none-any.whl", hash = "sha256:a31c71223cc71f07f9af2533af50c4cf1b910a85a730dd06a00aed053ea250fe", size = 13434, upload-time = "2025-07-21T09:24:58.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/9e/4285b91b03ad0d641fefaa69cc39cd7978106c40f729ac0052dd98a44204/cron_converter-1.3.1-py3-none-any.whl", hash = "sha256:60c645532802e12ad09097091a5ec83a79b9b1064374e89edb0facf2ba36a697", size = 14242, upload-time = "2025-12-02T19:03:32.306Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1108,14 +1112,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-pgactivity"
|
||||
version = "1.7.1"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/24/87ef93b1f4df5daa025739aba69b83a53814405dcb1a45e853177447b965/django_pgactivity-1.7.1.tar.gz", hash = "sha256:4d8aad75c2d48e1e79f39d9163b5f134f5867f65c75f08c37fa745d5e8f3d6fa", size = 13080, upload-time = "2024-12-16T02:01:44.621Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/2b/f8f91d90e29e8969a61b4267068b1566e6ec33a9cc83621721a45b9e6e6a/django_pgactivity-1.8.0.tar.gz", hash = "sha256:0988995caca9d5b1e7b60c0f335135cd85cb0625cddb1b95ce71bd73d6752d22", size = 12702, upload-time = "2025-11-30T23:26:39.384Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c2/9de50e5ed5e09cd7eb64e99f743087d4f6839917d7e69198860fc7442a58/django_pgactivity-1.7.1-py3-none-any.whl", hash = "sha256:0cd9e8dbaab7997410bd0fe490417d63f568fa27fd02e548bb3ea8dc73e0e319", size = 14422, upload-time = "2024-12-16T02:01:42.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/b1/966b4717bbcd002d0c6b90ba3af0361aee4b3d9b75246cb99d372ea42343/django_pgactivity-1.8.0-py3-none-any.whl", hash = "sha256:9b17f5d570266d26b3f8797379aeb721c95f60f999629a0648558374f0579eda", size = 14469, upload-time = "2025-11-30T23:26:38.237Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1223,15 +1227,15 @@ compatible-mypy = [
|
||||
|
||||
[[package]]
|
||||
name = "django-stubs-ext"
|
||||
version = "5.2.7"
|
||||
version = "5.2.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/6f/a0bab0e6a7676ab3ca02d51b459444e9bd6dd747e3a43b9c24cae6d0a1c6/django_stubs_ext-5.2.7.tar.gz", hash = "sha256:b690655bd4cb8a44ae57abb314e0995dc90414280db8f26fff0cb9fb367d1cac", size = 6524, upload-time = "2025-10-08T08:00:38.895Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/a2/d67f4a5200ff7626b104eddceaf529761cba4ed318a73ffdb0677551be73/django_stubs_ext-5.2.8.tar.gz", hash = "sha256:b39938c46d7a547cd84e4a6378dbe51a3dd64d70300459087229e5fee27e5c6b", size = 6487, upload-time = "2025-12-01T08:12:37.486Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/c9/60445606e26706d3fccadf3b80ee1a9f32c1012683ff2ada7580937b2da9/django_stubs_ext-5.2.7-py3-none-any.whl", hash = "sha256:0466a7132587d49c5bbe12082ac9824d117a0dedcad5d0ada75a6e0d3aca6f60", size = 9979, upload-time = "2025-10-08T08:00:37.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/2d/cb0151b780c3730cf0f2c0fcb1b065a5e88f877cf7a9217483c375353af1/django_stubs_ext-5.2.8-py3-none-any.whl", hash = "sha256:1dd5470c9675591362c78a157a3cf8aec45d0e7a7f0cf32f227a1363e54e0652", size = 9949, upload-time = "2025-12-01T08:12:36.397Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1820,14 +1824,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "incremental"
|
||||
version = "24.7.2"
|
||||
version = "24.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "setuptools" },
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157, upload-time = "2024-07-29T20:03:55.441Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516, upload-time = "2024-07-29T20:03:53.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/55/0f4df2a44053867ea9cbea73fc588b03c55605cd695cee0a3d86f0029cb2/incremental-24.11.0-py3-none-any.whl", hash = "sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3", size = 21109, upload-time = "2025-11-28T02:30:16.442Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1883,7 +1887,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "jsii"
|
||||
version = "1.119.0"
|
||||
version = "1.120.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
@@ -1894,9 +1898,9 @@ dependencies = [
|
||||
{ name = "typeguard" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/60/68bf5e617e94a584d2de483bd9162ad408a3a926e8de018bac36e375efec/jsii-1.119.0.tar.gz", hash = "sha256:9f87508908bfa51dd9aac59fdbbeff347ae76377758ea5e5f83f149052211514", size = 625546, upload-time = "2025-11-10T14:35:44.494Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/59/2b2c69a4c1cb91b757605bc6b543af055ee840b96406b2ae564bb86dab19/jsii-1.120.0.tar.gz", hash = "sha256:888855ddb7d124795e0c92c43d858d7d27f5372996210ec447a2a9af3a6c4315", size = 625665, upload-time = "2025-11-24T11:59:26.809Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/0e/ea1db75bcf2f1cf3556110ac60c88ef933bb5b910fa69ec008f726533a7f/jsii-1.119.0-py3-none-any.whl", hash = "sha256:9203200ed5289ecc6198783513cbd7abef53d4f6eac0046181d647fa56eda6ab", size = 601792, upload-time = "2025-11-10T14:35:43.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/98/56f5162c7a44b7cd2e967d113e001b5b2117eb84806eb9323f536f720e8e/jsii-1.120.0-py3-none-any.whl", hash = "sha256:5ba9b8a5420ce66f58b1a71ca57a4566c67f04b469140be335bd74abb91d5e0b", size = 601782, upload-time = "2025-11-24T11:59:25.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2376,42 +2380,42 @@ source = { git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.38.0"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763, upload-time = "2025-12-03T13:19:56.378Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459", size = 66357, upload-time = "2025-12-03T13:19:33.043Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.38.0"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322, upload-time = "2025-12-03T13:20:09.651Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514", size = 132413, upload-time = "2025-12-03T13:19:51.364Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.59b0"
|
||||
version = "0.60b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935, upload-time = "2025-12-03T13:20:12.395Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a", size = 219981, upload-time = "2025-12-03T13:19:53.585Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2625,14 +2629,14 @@ sdist = { url = "https://files.pythonhosted.org/packages/83/7f/6147cb842081b0b32
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-pool"
|
||||
version = "3.2.7"
|
||||
version = "3.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/8f/3ec52b17087c2ed5fa32b64fd4814dde964c9aa4bd49d0d30fc24725ca6d/psycopg_pool-3.2.7.tar.gz", hash = "sha256:a77d531bfca238e49e5fb5832d65b98e69f2c62bfda3d2d4d833696bdc9ca54b", size = 29765, upload-time = "2025-10-26T00:46:10.379Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/56/9a/9470d013d0d50af0da9c4251614aeb3c1823635cab3edc211e3839db0bcf/psycopg_pool-3.3.0.tar.gz", hash = "sha256:fa115eb2860bd88fce1717d75611f41490dec6135efb619611142b24da3f6db5", size = 31606, upload-time = "2025-12-01T11:34:33.11Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/59/74e752f605c6f0e351d4cf1c54fb9a1616dc800db4572b95bbfbb1a6225f/psycopg_pool-3.2.7-py3-none-any.whl", hash = "sha256:4b47bb59d887ef5da522eb63746b9f70e2faf967d34aac4f56ffc65e9606728f", size = 38232, upload-time = "2025-10-26T00:46:00.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/c3/26b8a0908a9db249de3b4169692e1c7c19048a9bc41a4d3209cee7dbb758/psycopg_pool-3.3.0-py3-none-any.whl", hash = "sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063", size = 39995, upload-time = "2025-12-01T11:34:29.761Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2644,6 +2648,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/d3/6308debad7afcdb3ea5f50b4b3d852f41eb566a311fbcb4da23755a28155/publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6", size = 7687, upload-time = "2019-01-15T07:52:22.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-ubjson"
|
||||
version = "0.16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/c7/28220d37e041fe1df03e857fe48f768dcd30cd151480bf6f00da8713214a/py-ubjson-0.16.1.tar.gz", hash = "sha256:b9bfb8695a1c7e3632e800fb83c943bf67ed45ddd87cd0344851610c69a5a482", size = 50316, upload-time = "2020-04-18T15:05:57.698Z" }
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
@@ -3033,39 +3043,39 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.29.0"
|
||||
version = "0.30.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710, upload-time = "2025-11-16T14:48:41.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582, upload-time = "2025-11-16T14:48:42.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172, upload-time = "2025-11-16T14:48:43.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586, upload-time = "2025-11-16T14:48:45.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339, upload-time = "2025-11-16T14:48:47.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201, upload-time = "2025-11-16T14:48:48.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095, upload-time = "2025-11-16T14:48:50.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077, upload-time = "2025-11-16T14:48:51.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548, upload-time = "2025-11-16T14:48:53.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661, upload-time = "2025-11-16T14:48:54.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937, upload-time = "2025-11-16T14:48:56.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496, upload-time = "2025-11-16T14:48:57.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886, upload-time = "2025-11-16T14:49:04.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262, upload-time = "2025-11-16T14:49:05.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826, upload-time = "2025-11-16T14:49:07.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234, upload-time = "2025-11-16T14:49:08.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008, upload-time = "2025-11-16T14:49:10.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569, upload-time = "2025-11-16T14:49:12.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188, upload-time = "2025-11-16T14:49:13.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587, upload-time = "2025-11-16T14:49:15.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641, upload-time = "2025-11-16T14:49:16.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683, upload-time = "2025-11-16T14:49:18.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730, upload-time = "2025-11-16T14:49:19.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361, upload-time = "2025-11-16T14:49:21.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3107,14 +3117,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3485,6 +3495,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "u-msgpack-python"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/9d/a40411a475e7d4838994b7f6bcc6bfca9acc5b119ce3a7503608c4428b49/u-msgpack-python-2.8.0.tar.gz", hash = "sha256:b801a83d6ed75e6df41e44518b4f2a9c221dc2da4bcd5380e3a0feda520bc61a", size = 18167, upload-time = "2023-05-18T09:28:12.187Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/512aeb40fd819f4660d00f96f5c7371ee36fc8c6b605128c5ee59e0b28c6/u_msgpack_python-2.8.0-py2.py3-none-any.whl", hash = "sha256:1d853d33e78b72c4228a2025b4db28cda81214076e5b0422ed0ae1b1b2bb586a", size = 10590, upload-time = "2023-05-18T09:28:10.323Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ua-parser"
|
||||
version = "1.0.1"
|
||||
@@ -3505,6 +3524,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d3/13adff37f15489c784cc7669c35a6c3bf94b87540229eedf52ef2a1d0175/ua_parser_builtins-0.18.0.post1-py3-none-any.whl", hash = "sha256:eb4f93504040c3a990a6b0742a2afd540d87d7f9f05fd66e94c101db1564674d", size = 86077, upload-time = "2024-12-05T18:44:36.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ujson"
|
||||
version = "5.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0", size = 7156583, upload-time = "2025-08-20T11:57:02.452Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ec/2de9dd371d52c377abc05d2b725645326c4562fc87296a8907c7bcdf2db7/ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53", size = 55435, upload-time = "2025-08-20T11:55:50.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/a4/f611f816eac3a581d8a4372f6967c3ed41eddbae4008d1d77f223f1a4e0a/ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752", size = 53193, upload-time = "2025-08-20T11:55:51.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/c5/c161940967184de96f5cbbbcce45b562a4bf851d60f4c677704b1770136d/ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50", size = 57603, upload-time = "2025-08-20T11:55:52.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/d6/c7b2444238f5b2e2d0e3dab300b9ddc3606e4b1f0e4bed5a48157cebc792/ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a", size = 59794, upload-time = "2025-08-20T11:55:53.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/a3/292551f936d3d02d9af148f53e1bc04306b00a7cf1fcbb86fa0d1c887242/ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03", size = 57363, upload-time = "2025-08-20T11:55:54.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/a6/82cfa70448831b1a9e73f882225980b5c689bf539ec6400b31656a60ea46/ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701", size = 1036311, upload-time = "2025-08-20T11:55:56.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/5c/96e2266be50f21e9b27acaee8ca8f23ea0b85cb998c33d4f53147687839b/ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60", size = 1195783, upload-time = "2025-08-20T11:55:58.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/20/78abe3d808cf3bb3e76f71fca46cd208317bf461c905d79f0d26b9df20f1/ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328", size = 1088822, upload-time = "2025-08-20T11:55:59.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/50/8856e24bec5e2fc7f775d867aeb7a3f137359356200ac44658f1f2c834b2/ujson-5.11.0-cp313-cp313-win32.whl", hash = "sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241", size = 39753, upload-time = "2025-08-20T11:56:01.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/d8/1baee0f4179a4d0f5ce086832147b6cc9b7731c24ca08e14a3fdb8d39c32/ujson-5.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0", size = 43866, upload-time = "2025-08-20T11:56:02.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/8c/6d85ef5be82c6d66adced3ec5ef23353ed710a11f70b0b6a836878396334/ujson-5.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9", size = 38363, upload-time = "2025-08-20T11:56:03.688Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unidecode"
|
||||
version = "1.4.0"
|
||||
|
||||
@@ -40,20 +40,16 @@ export class FormFixture extends PageFixture {
|
||||
|
||||
const role = await control.getAttribute("role");
|
||||
|
||||
let textbox: Locator;
|
||||
|
||||
if (role === "combobox") {
|
||||
// Comboboxes, such as our Query Language input need additional handling...
|
||||
const textbox = control.getByRole("textbox");
|
||||
|
||||
return textbox;
|
||||
} else {
|
||||
textbox = control;
|
||||
}
|
||||
|
||||
await expect(textbox, `Field (${fieldName}) should be visible`).toBeVisible();
|
||||
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
|
||||
|
||||
return textbox;
|
||||
return control;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,30 +1,117 @@
|
||||
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
|
||||
/**
|
||||
* @file ESLint Configuration
|
||||
*
|
||||
* @import { Config } from "eslint/config";
|
||||
*/
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import { createESLintPackageConfig, DefaultIgnorePatterns } from "@goauthentik/eslint-config";
|
||||
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* ESLint configuration for authentik's monorepo.
|
||||
* @typedef RestrictedImportEntry An entry describing a restricted import and it's remedy.
|
||||
* @property name {string} The restricted import name.
|
||||
* @property message {string} The message to show when the import is restricted.
|
||||
*/
|
||||
const ESLintConfig = createESLintPackageConfig({
|
||||
ignorePatterns: [
|
||||
"**/dist/**",
|
||||
"**/out/**",
|
||||
"**/vendored/**",
|
||||
"**/.wireit/**",
|
||||
"**/node_modules/",
|
||||
"**/.storybook/*",
|
||||
"coverage/",
|
||||
"src/locale-codes.ts",
|
||||
"storybook-static/",
|
||||
"src/locales/",
|
||||
"**/*.min.js",
|
||||
],
|
||||
});
|
||||
|
||||
export default tseslint.config(
|
||||
...ESLintConfig,
|
||||
/**
|
||||
* @typedef RestrictedImportPattern An entry describing a restricted import pattern and it's remedy.
|
||||
* @property regex {string} The restricted import pattern regex.
|
||||
* @property message {string} The message to show when the import is restricted.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef RestrictedImportsOptions An entry describing a restricted import rule.
|
||||
* @property patterns {RestrictedImportPattern[]} The restricted import patterns.
|
||||
* @property [paths] {RestrictedImportEntry[]} Optional restricted import paths.
|
||||
*/
|
||||
|
||||
const submodules = new Set(
|
||||
/** @type {const} */ ([
|
||||
"styles",
|
||||
"common",
|
||||
"elements",
|
||||
"components",
|
||||
"admin",
|
||||
"user",
|
||||
"flow",
|
||||
"rac",
|
||||
]),
|
||||
);
|
||||
|
||||
/**
|
||||
* @typedef {(typeof submodules) extends Set<infer U> ? U : never} SubModule
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SubModule} subpath
|
||||
* @param {Iterable<SubModule>} allowed
|
||||
* @returns {Config}
|
||||
*/
|
||||
function defineImportRestrictions(subpath, allowed) {
|
||||
const allowedSet = new Set(allowed);
|
||||
const restricted = submodules
|
||||
// ---
|
||||
.difference(allowedSet)
|
||||
.add(subpath);
|
||||
|
||||
/**
|
||||
* @type {RestrictedImportsOptions}
|
||||
*/
|
||||
const options = {
|
||||
patterns: Array.from(restricted, (mod) => ({
|
||||
regex: `#${mod}/.+`,
|
||||
message: `Cross-submodule import from #${mod} to #${subpath} is restricted. Consider moving the imported file to a #common if shared usage is intended.`,
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
rules: {
|
||||
"no-restricted-imports": ["warn", options],
|
||||
},
|
||||
files: Array.from(allowedSet, (mod) => `src/${mod}/**/*`),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Map<SubModule, SubModule[]>}
|
||||
*/
|
||||
const submoduleOrganization = new Map([
|
||||
["common", ["styles"]],
|
||||
["elements", ["styles", "common"]],
|
||||
["admin", ["styles", "common", "elements", "components"]],
|
||||
["user", ["styles", "common", "elements", "components"]],
|
||||
]);
|
||||
|
||||
/**
|
||||
* ESLint configuration for authentik's monorepo.
|
||||
* @type {Config[]}
|
||||
*/
|
||||
const eslintConfig = defineConfig(
|
||||
createESLintPackageConfig({
|
||||
parserOptions: {
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
ignorePatterns: [
|
||||
// ---
|
||||
...DefaultIgnorePatterns,
|
||||
"**/dist/**",
|
||||
"**/out/**",
|
||||
"**/vendored/**",
|
||||
"**/.wireit/**",
|
||||
"**/node_modules/",
|
||||
"**/.storybook/*",
|
||||
"coverage/",
|
||||
"src/locale-codes.ts",
|
||||
"playwright-report",
|
||||
"storybook-static/",
|
||||
"src/locales/",
|
||||
"**/*.min.js",
|
||||
],
|
||||
}),
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
@@ -33,27 +120,21 @@ export default tseslint.config(
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
"no-void": "off",
|
||||
"no-implicit-coercion": "off",
|
||||
"prefer-template": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"array-callback-return": "off",
|
||||
"block-scoped-var": "off",
|
||||
"consistent-return": "off",
|
||||
"func-names": "off",
|
||||
"guard-for-in": "off",
|
||||
"no-bitwise": "off",
|
||||
"no-div-regex": "off",
|
||||
"no-else-return": "off",
|
||||
"no-empty-function": "off",
|
||||
"no-empty-function": ["error", { allow: ["arrowFunctions"] }],
|
||||
"no-param-reassign": "off",
|
||||
"no-throw-literal": "off",
|
||||
// "no-var": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
"react/jsx-no-leaked-render": "off",
|
||||
"vars-on-top": "off",
|
||||
},
|
||||
},
|
||||
...Array.from(submoduleOrganization, ([target, allowed]) => {
|
||||
return defineImportRestrictions(target, allowed);
|
||||
}),
|
||||
{
|
||||
rules: {
|
||||
"vars-on-top": "off",
|
||||
},
|
||||
files: ["**/*.d.ts"],
|
||||
},
|
||||
);
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
2847
web/package-lock.json
generated
2847
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -99,10 +99,10 @@
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@goauthentik/api": "^2025.10.0-rc1-1760614339",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.2.2",
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
"@goauthentik/prettier-config": "^3.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.3.1",
|
||||
"@goauthentik/eslint-config": "^1.1.1",
|
||||
"@goauthentik/prettier-config": "^3.2.1",
|
||||
"@goauthentik/tsconfig": "^1.0.5",
|
||||
"@hcaptcha/types": "^1.1.0",
|
||||
"@lit/context": "^1.1.6",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@@ -116,8 +116,8 @@
|
||||
"@openlayers-elements/maps": "^0.4.0",
|
||||
"@patternfly/elements": "^4.2.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@sentry/browser": "^10.26.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@sentry/browser": "^10.29.0",
|
||||
"@storybook/addon-docs": "^10.0.8",
|
||||
"@storybook/addon-links": "^10.0.8",
|
||||
"@storybook/web-components": "^10.0.8",
|
||||
@@ -130,8 +130,8 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||
"@typescript-eslint/parser": "^8.47.0",
|
||||
"@vitest/browser": "^4.0.13",
|
||||
"@vitest/browser-playwright": "^4.0.13",
|
||||
"@vitest/browser": "^4.0.15",
|
||||
"@vitest/browser-playwright": "^4.0.15",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"change-case": "^5.4.4",
|
||||
@@ -143,8 +143,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"dompurify": "^3.3.0",
|
||||
"esbuild": "^0.27.0",
|
||||
"esbuild-plugin-copy": "^2.1.1",
|
||||
"esbuild": "^0.27.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-lit": "^2.1.1",
|
||||
"eslint-plugin-wc": "^3.0.2",
|
||||
@@ -157,12 +156,12 @@
|
||||
"lit": "^3.3.1",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.12.1",
|
||||
"mermaid": "^11.12.2",
|
||||
"node-domexception": "^2025.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pino": "^10.1.0",
|
||||
"pino-pretty": "^13.1.2",
|
||||
"playwright": "^1.55.1",
|
||||
"playwright": "^1.57.0",
|
||||
"prettier": "^3.6.2",
|
||||
"pseudolocale": "^2.2.0",
|
||||
"rapidoc": "^9.3.8",
|
||||
@@ -183,13 +182,13 @@
|
||||
"turnstile-types": "^1.2.3",
|
||||
"type-fest": "^5.2.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.47.0",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite": "^7.2.4",
|
||||
"vitest": "^4.0.13",
|
||||
"vite": "^7.2.6",
|
||||
"vitest": "^4.0.15",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
"yaml": "^2.8.1"
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.27.0",
|
||||
@@ -198,7 +197,7 @@
|
||||
"@rollup/rollup-darwin-arm64": "^4.53.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.53.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.53.3",
|
||||
"chromedriver": "^142.0.3"
|
||||
"chromedriver": "^143.0.0"
|
||||
},
|
||||
"wireit": {
|
||||
"build": {
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"import": "./*/index.js"
|
||||
},
|
||||
".": {
|
||||
"import": "./index.js",
|
||||
"types": "./out/index.d.ts"
|
||||
"types": "./out/index.d.ts",
|
||||
"import": "./index.js"
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@goauthentik/tsconfig": "^1.0.5",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/semver": "^7.7.1",
|
||||
"semver": "^7.7.3",
|
||||
|
||||
@@ -167,7 +167,9 @@ export class Lexer {
|
||||
if (Array.isArray(token)) {
|
||||
this.tokens = token.slice(1);
|
||||
return token[0];
|
||||
} else return token;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
} else {
|
||||
if (this.index !== index) this.remove = 0;
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"formdata-polyfill": "^2025.11.0",
|
||||
"globby": "16.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.53.3",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
import { fromByteArray } from "base64-js";
|
||||
import $ from "jquery";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
|
||||
interface GlobalAuthentik {
|
||||
brand: {
|
||||
branding_logo: string;
|
||||
|
||||
@@ -41,7 +41,7 @@ export default defineConfig({
|
||||
isEnabled() {
|
||||
return true;
|
||||
},
|
||||
log: (name, severity, message, args) => {
|
||||
log: (name, severity, message, _args) => {
|
||||
let logger = LoggerCache.get(name);
|
||||
|
||||
if (!logger) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import { BuildIdentifier } from "@goauthentik/core/version/node";
|
||||
|
||||
import { deepmerge } from "deepmerge-ts";
|
||||
import esbuild from "esbuild";
|
||||
import { copy } from "esbuild-plugin-copy";
|
||||
|
||||
/// <reference types="../types/esbuild.js" />
|
||||
|
||||
@@ -37,6 +36,22 @@ const publicBundledDefinitions = Object.fromEntries(
|
||||
);
|
||||
logger.info(publicBundledDefinitions, "Bundle definitions");
|
||||
|
||||
/**
|
||||
* @typedef {[from: string, to: string]} SourceDestinationPair
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {SourceDestinationPair[]}
|
||||
*/
|
||||
const assets = [
|
||||
[
|
||||
path.join(path.dirname(EntryPoint.StandaloneLoading.in), "startup"),
|
||||
path.dirname(EntryPoint.StandaloneLoading.out),
|
||||
],
|
||||
[path.resolve(PackageRoot, "src", "assets", "images"), "./assets/images"],
|
||||
[path.resolve(PackageRoot, "icons"), "./assets/icons"],
|
||||
];
|
||||
|
||||
/**
|
||||
* @type {Readonly<BuildOptions>}
|
||||
*/
|
||||
@@ -63,22 +78,42 @@ const BASE_ESBUILD_OPTIONS = {
|
||||
".svg": "file",
|
||||
},
|
||||
plugins: [
|
||||
copy({
|
||||
assets: [
|
||||
{
|
||||
from: path.join(path.dirname(EntryPoint.StandaloneLoading.in), "startup", "**"),
|
||||
to: path.dirname(EntryPoint.StandaloneLoading.out),
|
||||
},
|
||||
{
|
||||
from: path.resolve(PackageRoot, "src", "assets", "images", "**"),
|
||||
to: "./assets/images",
|
||||
},
|
||||
{
|
||||
from: path.resolve(PackageRoot, "icons", "*"),
|
||||
to: "./assets/icons",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
name: "copy",
|
||||
setup(build) {
|
||||
build.onEnd(async () => {
|
||||
/**
|
||||
* @type {import('esbuild').PartialMessage[]}
|
||||
*/
|
||||
const errors = [];
|
||||
|
||||
/**
|
||||
* @param {SourceDestinationPair} pair
|
||||
*/
|
||||
const copy = ([from, to]) => {
|
||||
const resolvedDestination = path.resolve(DistDirectory, to);
|
||||
|
||||
logger.debug(`📋 Copying assets from ${from} to ${to}`);
|
||||
|
||||
return fs
|
||||
.cp(from, resolvedDestination, { recursive: true })
|
||||
.catch((error) => {
|
||||
errors.push({
|
||||
text: `Failed to copy assets from ${from} to ${to}: ${error}`,
|
||||
location: {
|
||||
file: from,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
await Promise.all(assets.map(copy));
|
||||
|
||||
return { errors };
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
mdxPlugin({
|
||||
root: MonoRepoRoot,
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "#elements/router/Route";
|
||||
import { SidebarItemProperties } from "#elements/sidebar/SidebarItem";
|
||||
import { LitPropertyRecord } from "#elements/types";
|
||||
|
||||
import { SidebarItemProperties } from "#admin/sidebar/SidebarItem";
|
||||
|
||||
import { spread } from "@open-wc/lit-helpers";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -18,6 +19,14 @@ export type SidebarEntry = [
|
||||
children?: SidebarEntry[],
|
||||
];
|
||||
|
||||
/**
|
||||
* Recursively renders a collection of sidebar entries.
|
||||
*/
|
||||
export function renderSidebarItems(entries: readonly SidebarEntry[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
return repeat(entries, ([path, label]) => path || label, renderSidebarItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively renders a sidebar entry.
|
||||
*/
|
||||
@@ -44,13 +53,6 @@ export function renderSidebarItem([
|
||||
</ak-sidebar-item>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively renders a collection of sidebar entries.
|
||||
*/
|
||||
export function renderSidebarItems(entries: readonly SidebarEntry[]) {
|
||||
return repeat(entries, ([path, label]) => path || label, renderSidebarItem);
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export const createAdminSidebarEntries = (): readonly SidebarEntry[] => [
|
||||
[null, msg("Dashboards"), { "?expanded": true }, [
|
||||
|
||||
@@ -5,8 +5,8 @@ import "#elements/messages/MessageContainer";
|
||||
import "#elements/notifications/APIDrawer";
|
||||
import "#elements/notifications/NotificationDrawer";
|
||||
import "#elements/router/RouterOutlet";
|
||||
import "#elements/sidebar/Sidebar";
|
||||
import "#elements/sidebar/SidebarItem";
|
||||
import "#admin/sidebar/Sidebar";
|
||||
import "#admin/sidebar/SidebarItem";
|
||||
|
||||
import {
|
||||
createAdminSidebarEnterpriseEntries,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import "#admin/admin-overview/AdminOverviewPage";
|
||||
|
||||
import { globalAK } from "#common/global";
|
||||
|
||||
import { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "#elements/router/Route";
|
||||
|
||||
import { html } from "lit";
|
||||
@@ -186,14 +184,3 @@ export const ROUTES: Route[] = [
|
||||
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
|
||||
}),
|
||||
];
|
||||
|
||||
/**
|
||||
* Application route helpers.
|
||||
*
|
||||
* @TODO: This API isn't quite right yet. Revisit after the hash router is replaced.
|
||||
*/
|
||||
export const ApplicationRoute = {
|
||||
EditURL(slug: string, base = globalAK().api.base) {
|
||||
return `${base}if/admin/#/core/applications/${slug}`;
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -2,8 +2,8 @@ import "#elements/Tabs";
|
||||
import "#elements/buttons/ActionButton/index";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/events/LogViewer";
|
||||
import "#elements/tasks/ScheduleList";
|
||||
import "#elements/tasks/TaskList";
|
||||
import "#admin/tasks/ScheduleList";
|
||||
import "#admin/tasks/TaskList";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
@@ -22,23 +22,20 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-system-tasks")
|
||||
export class SystemTasksPage extends AKElement {
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFList,
|
||||
PFBanner,
|
||||
PFPage,
|
||||
PFContent,
|
||||
PFButton,
|
||||
PFDescriptionList,
|
||||
PFGrid,
|
||||
PFCard,
|
||||
];
|
||||
}
|
||||
public static styles: CSSResult[] = [
|
||||
// ---
|
||||
PFList,
|
||||
PFBanner,
|
||||
PFPage,
|
||||
PFContent,
|
||||
PFButton,
|
||||
PFDescriptionList,
|
||||
PFGrid,
|
||||
PFCard,
|
||||
];
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<main part="main">
|
||||
|
||||
@@ -19,7 +19,7 @@ export class FipsStatusCard extends AdminStatusCard<SystemInfo> {
|
||||
protected statusSummary?: string;
|
||||
|
||||
async getPrimaryValue(): Promise<SystemInfo> {
|
||||
return await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
|
||||
return new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
|
||||
}
|
||||
|
||||
setStatus(summary: string, content: StatusContent): Promise<AdminStatus> {
|
||||
|
||||
@@ -7,13 +7,13 @@ import "#elements/buttons/SpinnerButton/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EventWithContext } from "#common/events";
|
||||
import { EventGeo, renderEventUser } from "#common/events/utils";
|
||||
import { actionToLabel } from "#common/labels";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import Styles from "#admin/admin-overview/cards/RecentEventsCard.css";
|
||||
import { EventGeo, renderEventUser } from "#admin/events/utils";
|
||||
|
||||
import { Event, EventsApi } from "@goauthentik/api";
|
||||
|
||||
|
||||
@@ -8,19 +8,15 @@ import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#elements/utils/TimeDeltaHelp";
|
||||
import "./AdminSettingsFooterLinks.js";
|
||||
import "#elements/CodeMirror";
|
||||
|
||||
import { akFooterLinkInput, IFooterLinkInput } from "./AdminSettingsFooterLinks.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { CodeMirrorMode } from "#elements/CodeMirror";
|
||||
import { Form } from "#elements/forms/Form";
|
||||
|
||||
import { AdminApi, FooterLink, Settings, SettingsRequest } from "@goauthentik/api";
|
||||
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
@@ -249,16 +245,33 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
value="${settings.defaultTokenLength ?? 60}"
|
||||
help=${msg("Default length of generated tokens")}
|
||||
></ak-number-input>
|
||||
<ak-form-element-horizontal label=${msg("Flags")} name="flags" required>
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(settings?.flags ?? {})}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Modify flags to opt into new authentik behaviours early.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group
|
||||
label=${msg("Flags")}
|
||||
description=${msg(
|
||||
"Flags allow you to enable new functionality and behaviour in authentik early.",
|
||||
)}
|
||||
>
|
||||
<div class="pf-c-form">
|
||||
<ak-switch-input
|
||||
name="flags.policiesBufferedAccessView"
|
||||
?checked=${settings?.flags.policiesBufferedAccessView ?? false}
|
||||
label=${msg("Buffer PolicyAccessVew requests")}
|
||||
help=${msg(
|
||||
"When enabled, parallel requests for application authorization will be buffered instead of conflicting with other flows.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="flags.flowsRefreshOthers"
|
||||
?checked=${settings?.flags.flowsRefreshOthers ?? false}
|
||||
label=${msg("Refresh other flow tabs upon authentication")}
|
||||
help=${msg(
|
||||
"When enabled, other flow tabs in a session will refresh upon a successful authentication.",
|
||||
)}
|
||||
>
|
||||
</ak-switch-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import "#elements/Alert";
|
||||
import "#elements/forms/FormGroup";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/forms/ProxyForm";
|
||||
import "#admin/forms/ProxyForm";
|
||||
import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/ak-search-select";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
@@ -184,7 +184,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
<ak-file-search-input
|
||||
name="metaIcon"
|
||||
label=${msg("Icon")}
|
||||
value=${ifDefined(this.instance?.metaIcon)}
|
||||
value=${ifPresent(this.instance?.metaIcon)}
|
||||
.usage=${AdminFileListUsageEnum.Media}
|
||||
help=${msg(
|
||||
"Select from uploaded files, or type a Font Awesome icon (fa://fa-icon-name) or URL.",
|
||||
|
||||
@@ -5,7 +5,6 @@ import "#elements/forms/SearchSelect/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { CodeMirrorMode } from "#elements/CodeMirror";
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import { ApplicationEntitlement, CoreApi } from "@goauthentik/api";
|
||||
@@ -64,7 +63,7 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
mode="yaml"
|
||||
value="${YAML.stringify(this.instance?.attributes ?? {})}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
|
||||
@@ -5,16 +5,15 @@ import "#components/ak-status-label";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/forms/ProxyForm";
|
||||
import "#admin/forms/ProxyForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
import { PolicyBindingCheckTarget } from "#common/policies/utils";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { PolicyBindingCheckTarget } from "#admin/policies/utils";
|
||||
|
||||
import {
|
||||
ApplicationEntitlement,
|
||||
CoreApi,
|
||||
|
||||
@@ -2,18 +2,18 @@ import "#admin/applications/wizard/ak-wizard-title";
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/forms/FormGroup";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/wizard/TypeCreateWizardPage";
|
||||
import "#admin/wizard/TypeCreateWizardPage";
|
||||
|
||||
import { applicationWizardProvidersContext } from "../ContextIdentity.js";
|
||||
import { type LocalTypeCreate } from "./ProviderChoices.js";
|
||||
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage";
|
||||
|
||||
import type { NavigableButton, WizardButton } from "#components/ak-wizard/types";
|
||||
|
||||
import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep";
|
||||
import { TypeCreateWizardPageLayouts } from "#admin/wizard/TypeCreateWizardPage";
|
||||
|
||||
import { TypeCreate } from "@goauthentik/api";
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import "#elements/forms/SearchSelect/index";
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { docLink } from "#common/global";
|
||||
|
||||
import { CodeMirrorMode } from "#elements/CodeMirror";
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import { BlueprintFile, BlueprintInstance, ManagedApi } from "@goauthentik/api";
|
||||
@@ -175,7 +174,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
|
||||
${this.source === blueprintSource.internal
|
||||
? html`<ak-form-element-horizontal label=${msg("Blueprint")} name="content">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
mode="yaml"
|
||||
raw
|
||||
value="${ifDefined(this.instance?.content)}"
|
||||
></ak-codemirror>
|
||||
@@ -188,7 +187,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Context")} name="context">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
mode="yaml"
|
||||
value="${YAML.stringify(this.instance?.context ?? {})}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
|
||||
@@ -5,7 +5,7 @@ import "#elements/buttons/ActionButton/index";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/tasks/TaskList";
|
||||
import "#admin/tasks/TaskList";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
import "#elements/ak-mdx/ak-mdx";
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import "#components/ak-file-search-input";
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { DefaultBrand } from "#common/ui/config";
|
||||
|
||||
import { CodeMirrorMode } from "#elements/CodeMirror";
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
@@ -133,7 +132,7 @@ export class BrandForm extends ModelForm<Brand, string> {
|
||||
|
||||
<ak-codemirror
|
||||
id="branding-custom-css"
|
||||
mode=${CodeMirrorMode.CSS}
|
||||
mode="css"
|
||||
value="${this.instance?.brandingCustomCss ??
|
||||
DefaultBrand.brandingCustomCss}"
|
||||
>
|
||||
@@ -304,11 +303,12 @@ export class BrandForm extends ModelForm<Brand, string> {
|
||||
<ak-codemirror
|
||||
id="attributes"
|
||||
name="attributes"
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
mode="yaml"
|
||||
value="${YAML.stringify(this.instance?.attributes ?? {})}"
|
||||
aria-describedby="attributes-help"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
<p class="pf-c-form__helper-text" id="attributes-help">
|
||||
${msg(
|
||||
"Set custom attributes using YAML or JSON. Any attributes set here will be inherited by users, if the request is handled by this brand.",
|
||||
)}
|
||||
|
||||
@@ -28,9 +28,9 @@ const metadata: Meta<AkCryptoCertificateSearch> = {
|
||||
],
|
||||
},
|
||||
argTypes: {
|
||||
// Typescript is unaware that arguments for components are treated as properties, and
|
||||
// properties are typically renamed to lower case, even if the variable is not.
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error Typescript is unaware that arguments for components
|
||||
// are treated as properties, and properties are typically renamed to lower case,
|
||||
// even if the variable is not.
|
||||
nokey: {
|
||||
control: "boolean",
|
||||
description:
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import "#admin/common/ak-license-notice";
|
||||
import "#admin/endpoints/connectors/agent/AgentConnectorForm";
|
||||
import "#elements/forms/ProxyForm";
|
||||
import "#elements/wizard/FormWizardPage";
|
||||
import "#elements/wizard/TypeCreateWizardPage";
|
||||
import "#elements/wizard/Wizard";
|
||||
import "#admin/forms/ProxyForm";
|
||||
import "#admin/wizard/FormWizardPage";
|
||||
import "#admin/wizard/TypeCreateWizardPage";
|
||||
import "#admin/wizard/Wizard";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { Wizard } from "#elements/wizard/Wizard";
|
||||
|
||||
import { Wizard } from "#admin/wizard/Wizard";
|
||||
|
||||
import { EndpointsApi, TypeCreate } from "@goauthentik/api";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "#admin/endpoints/connectors/ConnectorWizard";
|
||||
import "#admin/endpoints/connectors/agent/AgentConnectorForm";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ProxyForm";
|
||||
import "#admin/forms/ProxyForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
@@ -5,7 +5,6 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { ModalButton } from "#elements/buttons/ModalButton";
|
||||
import { CodeMirrorMode } from "#elements/CodeMirror";
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
|
||||
import { EndpointsAgentsConnectorsMdmConfigCreateRequest, EndpointsApi } from "@goauthentik/api";
|
||||
@@ -25,7 +24,7 @@ export class ConfigModal extends ModalButton {
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("ak-modal-show", (e) => {
|
||||
this.addEventListener("ak-modal-show", () => {
|
||||
if (!this.request) return;
|
||||
new EndpointsApi(DEFAULT_CONFIG)
|
||||
.endpointsAgentsConnectorsMdmConfigCreate(this.request)
|
||||
@@ -40,7 +39,7 @@ export class ConfigModal extends ModalButton {
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.XML}
|
||||
mode="xml"
|
||||
readonly
|
||||
value="${ifDefined(this.config)}"
|
||||
></ak-codemirror>
|
||||
|
||||
@@ -3,15 +3,14 @@ import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#admin/users/UserForm";
|
||||
import "#components/ak-status-label";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/forms/ProxyForm";
|
||||
import "#admin/forms/ProxyForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PolicyBindingCheckTarget, PolicyBindingCheckTargetToLabel } from "#common/policies/utils";
|
||||
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { PolicyBindingCheckTarget, PolicyBindingCheckTargetToLabel } from "#admin/policies/utils";
|
||||
|
||||
import { PoliciesApi, PolicyBinding } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@@ -22,6 +21,8 @@ import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
|
||||
|
||||
@customElement("ak-bound-device-users-list")
|
||||
export class BoundDeviceUsersList extends Table<PolicyBinding> {
|
||||
static styles: CSSResult[] = [...super.styles, PFSpacing];
|
||||
|
||||
@property()
|
||||
target?: string;
|
||||
|
||||
@@ -30,10 +31,6 @@ export class BoundDeviceUsersList extends Table<PolicyBinding> {
|
||||
|
||||
order = "order";
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFSpacing);
|
||||
}
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<PolicyBinding>> {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
|
||||
@@ -18,7 +18,7 @@ export class DeviceAddHowTo extends ModalButton {
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("ak-modal-show", (e) => {
|
||||
this.addEventListener("ak-modal-show", () => {
|
||||
new EndpointsApi(DEFAULT_CONFIG).endpointsConnectorsList().then((e) => {
|
||||
this.connectors = e.results;
|
||||
});
|
||||
@@ -51,7 +51,7 @@ export class DeviceAddHowTo extends ModalButton {
|
||||
${this.connectors.length === 0
|
||||
? this.renderNone()
|
||||
: html` <ak-tabs part="tabs" vertical>
|
||||
${this.connectors.map((c, idx) => {
|
||||
${this.connectors.map((c) => {
|
||||
return html`<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
|
||||
@@ -7,7 +7,6 @@ import "#elements/CodeMirror";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { CodeMirrorMode } from "#elements/CodeMirror";
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import { EndpointDevice, EndpointsApi } from "@goauthentik/api";
|
||||
@@ -53,7 +52,7 @@ export class EndpointDeviceForm extends ModelForm<EndpointDevice, string> {
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
mode="yaml"
|
||||
value="${YAML.stringify(this.instance?.attributes ?? {})}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
|
||||
@@ -6,6 +6,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EventWithContext } from "#common/events";
|
||||
import { EventGeo, renderEventUser } from "#common/events/utils";
|
||||
import { actionToLabel } from "#common/labels";
|
||||
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
@@ -13,8 +14,6 @@ import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table
|
||||
import { TablePage } from "#elements/table/TablePage";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { EventGeo, renderEventUser } from "#admin/events/utils";
|
||||
|
||||
import { Event, EventsApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
@@ -2,6 +2,7 @@ import "#components/ak-event-info";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EventWithContext } from "#common/events";
|
||||
import { EventGeo, renderEventUser } from "#common/events/utils";
|
||||
import { actionToLabel } from "#common/labels";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
@@ -9,8 +10,6 @@ import { Timestamp } from "#elements/table/shared";
|
||||
|
||||
import { setPageDetails } from "#components/ak-page-navbar";
|
||||
|
||||
import { EventGeo, renderEventUser } from "#admin/events/utils";
|
||||
|
||||
import { EventsApi, EventToJSON } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
|
||||
@@ -5,7 +5,7 @@ import "#components/ak-status-label";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/tasks/TaskList";
|
||||
import "#admin/tasks/TaskList";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
@@ -4,7 +4,7 @@ import "#elements/buttons/ActionButton/index";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/tasks/TaskList";
|
||||
import "#admin/tasks/TaskList";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
@@ -5,7 +5,7 @@ import "#admin/stages/StageWizard";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/forms/ProxyForm";
|
||||
import "#admin/forms/ProxyForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
@@ -145,7 +145,7 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
|
||||
</ak-stage-binding-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Bind existing stage")}
|
||||
${msg("Bind existing Stage")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
@@ -166,7 +166,7 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
<ak-stage-binding-form slot="form" targetPk=${ifDefined(this.target)}>
|
||||
</ak-stage-binding-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Bind existing stage")}
|
||||
${msg("Bind existing Stage")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${super.renderToolbar()}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { SentryIgnoredError } from "#common/sentry/index";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
|
||||
import { AKLabel } from "#components/ak-label";
|
||||
|
||||
import { Flow, FlowImportResult, FlowsApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -66,20 +68,34 @@ export class FlowImportForm extends Form<Flow> {
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${msg("Flow")} name="flow">
|
||||
<input type="file" value="" class="pf-c-form-control" />
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(".yaml files, which can be found in the Example Flows documentation")}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("See more here:")}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${docLink("/add-secure-apps/flows-stages/flow/examples/flows/")}
|
||||
>${msg("Documentation")}</a
|
||||
>
|
||||
</p>
|
||||
return html`<ak-form-element-horizontal name="flow">
|
||||
<div slot="label" class="pf-c-form__group-label">
|
||||
${AKLabel({ htmlFor: "flow" }, msg("Flow"))}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
value=""
|
||||
class="pf-c-form-control"
|
||||
id="flow"
|
||||
name="flow"
|
||||
aria-describedby="flow-help"
|
||||
/>
|
||||
|
||||
<div id="flow-help">
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(".yaml files, which can be found in the Example Flows documentation")}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Read more about")}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${docLink("/add-secure-apps/flows-stages/flow/examples/flows/")}
|
||||
>${msg("Flow Examples")}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
${this.result ? this.renderResult() : nothing}`;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user