Compare commits

..

47 Commits

Author SHA1 Message Date
Teffen Ellis
dc794d927e Fix import path. 2025-12-06 18:11:31 +01:00
Teffen Ellis
01b91e9dd1 Add ESLint rule. 2025-12-06 06:44:52 +01:00
Teffen Ellis
6efb3f5fb0 web: Enforce code organization. 2025-12-06 06:44:41 +01:00
Teffen Ellis
4b24a7ddc1 Replace esbuild-copy-plugin with fs module. 2025-12-06 06:40:58 +01:00
Teffen Ellis
b6b423bee9 Remove unused. 2025-12-06 06:40:58 +01:00
Teffen Ellis
860a43e56a Fix define before use. 2025-12-06 06:40:57 +01:00
Teffen Ellis
c1cbd0623c Fix unused parameters. 2025-12-06 06:40:57 +01:00
Teffen Ellis
0893031cab Fix unnamed functions. 2025-12-06 05:48:42 +01:00
Teffen Ellis
89868276da Fix empty functions 2025-12-06 05:48:42 +01:00
Teffen Ellis
dd40f60c88 Fix ts ignore comments. 2025-12-06 05:48:41 +01:00
Teffen Ellis
73be48c179 Fix linter. 2025-12-06 05:48:41 +01:00
Teffen Ellis
fc27dfeeb7 Fix config. 2025-12-06 05:48:29 +01:00
Jens L.
dbbfb3cf19 root: fix missing authentik_device cookie causing error (#18642)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-06 03:00:56 +01:00
Jens L.
6d7249ea56 enterprise/stages/mtls: fix traefik certificate parsing (#18607)
* enterprise/stages/mtls: fix traefik certificate parsing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add links for relevant docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-05 19:06:20 +01:00
Dewi Roberts
a07e820bce wed/admin: change s to S in "Stage" (#18632)
change s to S in "Stage"
2025-12-05 16:11:52 +00:00
Jens L.
31186baf25 flows: refresh unauthenticated tabs (#18621)
* flows: implement signaling

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add flag

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* better flag configuration

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Update web/src/flow/FlowExecutor.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Jens L. <jens@beryju.org>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-12-05 16:03:16 +01:00
Jens L.
024e6c1961 flows: keep ?next url when using cancel (#18619)
keep ?next url when using cancel

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-05 15:35:15 +01:00
authentik-automation[bot]
1244a40ffb core, web: update translations (#18620)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-12-05 15:18:42 +01:00
dependabot[bot]
dcfe722f5c ci: bump actions/setup-node from 6.0.0 to 6.1.0 (#18552)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](2028fbc5c2...395ad32622)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-05 14:37:31 +01:00
dependabot[bot]
6b1171aac8 core: bump goauthentik/fips-debian from cf233be to a80dbbd (#18594)
Bumps goauthentik/fips-debian from `cf233be` to `a80dbbd`.

---
updated-dependencies:
- dependency-name: goauthentik/fips-debian
  dependency-version: trixie-slim-fips
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-05 14:37:12 +01:00
dependabot[bot]
b2d5519611 web: bump @sentry/browser from 10.28.0 to 10.29.0 in /web in the sentry group across 1 directory (#18623)
web: bump @sentry/browser in /web in the sentry group across 1 directory

Bumps the sentry group with 1 update in the /web directory: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 10.28.0 to 10.29.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.28.0...10.29.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-version: 10.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-05 14:36:55 +01:00
Dewi Roberts
1620a96cd4 website/docs: adds note about ak_create_jwt function (#18614)
* Adds note

* Apply suggestion from @tanberry

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-12-05 09:35:53 +00:00
Jens L.
a42fc4b741 api: fix IPC auth (#18612)
* api: fix IPC auth

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-04 22:50:50 +01:00
dependabot[bot]
9b822ce0fd web: bump mermaid from 11.12.1 to 11.12.2 in /web (#18602)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.12.1 to 11.12.2.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.12.1...mermaid@11.12.2)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.12.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 14:27:27 -05:00
Teffen Ellis
05c30af790 web: Codemirror fixes (#18610)
* web: Dynamic Loading of Codemirror

* Clarify error.

* Fix labels, links

* Fix key maps, tabbing

* Remove dupe.

* Update web/src/elements/codemirror/editor.ts

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

* Fix inversion of opacity.

* Format.

* Fix import.

* Fix imports.

* Fix static styles using getters.

- Seems to be a merge conflict from long ago.

* Fix typo.

* Fix capitalization.

---------

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2025-12-04 19:15:43 +00:00
dependabot[bot]
6683d9943c web: bump packages in /web (#18604)
* web: bump playwright from 1.56.1 to 1.57.0 in /web

Bumps [playwright](https://github.com/microsoft/playwright) from 1.56.1 to 1.57.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.56.1...v1.57.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump Playwright related.

* Fix package upgrade log jam.

* Format.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
2025-12-04 19:15:14 +00:00
Dominic R
17ef75c19f website/docs: expressions: fix markdown (#18613) 2025-12-04 18:19:42 +00:00
Dewi Roberts
d8428bf59a website/docs: add missing API sidebar entry (#18586)
Adds missing sidebar entry
2025-12-04 11:53:51 -05:00
dependabot[bot]
3ef06094b5 web: bump yaml from 2.8.1 to 2.8.2 in /web (#18605)
Bumps [yaml](https://github.com/eemeli/yaml) from 2.8.1 to 2.8.2.
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.8.1...v2.8.2)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 11:33:44 -05:00
Marc 'risson' Schmitt
6b22487406 web/elements: update AppIcon story with files change (#18608)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-04 16:28:57 +00:00
Jens L.
0fa412e782 api: test action decorator (#18583)
* api: validate usage of action decorator

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rework auth

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	authentik/enterprise/endpoints/connectors/agent/auth.py

* refactor auth

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fixup things

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix outpost token

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-04 16:44:04 +01:00
Jens L.
334c0175f9 crypto: separate permissions for certificate and private keydownload (#18588)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-04 16:31:52 +01:00
dependabot[bot]
3c2f39559f core: bump github.com/spf13/cobra from 1.10.1 to 1.10.2 (#18592)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 15:31:18 +00:00
dependabot[bot]
d05ad4403b core: bump goauthentik.io/api/v3 from 3.2025120.15 to 3.2025120.16 (#18591)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 15:55:19 +01:00
authentik-automation[bot]
10866f9dfc core, web: update translations (#18587)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-12-04 15:54:09 +01:00
Marcelo Elizeche Landó
97f0c6475d core: bump boto3 from 1.40.75 to v1.42.1 (#18571) 2025-12-04 15:47:59 +01:00
Marcelo Elizeche Landó
0f6cb9183e core: bump asgiref from 3.10.0 to v3.11.0 (#18568) 2025-12-04 15:47:40 +01:00
Marcelo Elizeche Landó
499c1b6fab core: bump autobahn from 25.10.2 to v25.11.1 (#18569) 2025-12-04 15:47:21 +01:00
Marcelo Elizeche Landó
362d67ca6e core: bump blessed from 1.24.0 to v1.25.0 (#18570) 2025-12-04 15:47:09 +01:00
Marcelo Elizeche Landó
abe944b8c9 core: bump cron-converter from 1.2.2 to v1.3.1 (#18572) 2025-12-04 15:47:05 +01:00
Marcelo Elizeche Landó
bba9643864 core: bump django-stubs-ext from 5.2.7 to v5.2.8 (#18574) 2025-12-04 15:46:30 +01:00
Marcelo Elizeche Landó
467af902f1 core: bump django-pgactivity from 1.7.1 to v1.8.0 (#18573) 2025-12-04 15:45:59 +01:00
Marcelo Elizeche Landó
e28a8aacc7 core: bump rpds-py from 0.29.0 to v0.30.0 (#18579) 2025-12-04 15:45:43 +01:00
Marcelo Elizeche Landó
af0444b0dd core: bump opentelemetry-api from 1.38.0 to v1.39.0 (#18577) 2025-12-04 15:45:33 +01:00
Marcelo Elizeche Landó
8fcf60ecce core: bump incremental from 24.7.2 to v24.11.0 (#18575) 2025-12-04 15:45:21 +01:00
Marcelo Elizeche Landó
10ebbcfd61 core: bump jsii from 1.119.0 to v1.120.0 (#18576) 2025-12-04 15:45:07 +01:00
Marcelo Elizeche Landó
6a1bde1fd8 core: bump psycopg-pool from 3.2.7 to v3.3.0 (#18578) 2025-12-04 15:45:03 +01:00
256 changed files with 4912 additions and 3968 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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):

View File

@@ -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)

View 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),
)

View File

@@ -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"""

View File

@@ -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:

View File

@@ -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:

View File

@@ -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", **_):

View File

@@ -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 = [

View File

@@ -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()

View File

@@ -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",
},
),
]

View File

@@ -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")),
]

View File

@@ -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(),
[

View File

@@ -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:

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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"))

View File

@@ -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"""

View File

@@ -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,
}
)

View File

@@ -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

View File

@@ -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,
},
)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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

View File

@@ -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(

View File

@@ -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:

View File

@@ -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)

View File

@@ -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",

View 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"})

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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 = {}

View File

@@ -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 = []

View File

@@ -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 = []

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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"

View File

@@ -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;
};
/**

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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;

View File

@@ -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"
},

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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,
}),

View File

@@ -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 }, [

View File

@@ -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,

View File

@@ -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;

View File

@@ -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">

View File

@@ -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> {

View File

@@ -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";

View File

@@ -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>
`;
}
}

View File

@@ -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.",

View File

@@ -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>

View File

@@ -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,

View File

@@ -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";

View File

@@ -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>

View File

@@ -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";

View File

@@ -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.",
)}

View File

@@ -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:

View File

@@ -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";

View File

@@ -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";

View File

@@ -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>

View File

@@ -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()),

View File

@@ -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"

View File

@@ -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>

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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()}

View File

@@ -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:")}&nbsp;
<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")}&nbsp;
<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