Compare commits

..

26 Commits

Author SHA1 Message Date
Marc 'risson' Schmitt
7b942ab9f2 wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-05 15:53:19 +02:00
Marc 'risson' Schmitt
34d575ec9f start on application router
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-05 15:53:19 +02:00
Marc 'risson' Schmitt
c1ce21a037 continue on handlers
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-05 15:53:18 +02:00
Marc 'risson' Schmitt
59253c4065 wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-05 15:53:18 +02:00
Marc 'risson' Schmitt
76e6c33b10 add container
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-05 15:53:17 +02:00
Marc 'risson' Schmitt
76dbfd4051 outpost basics and refresh logic
commit 04669c9f857ecb0b47a5303958bf02de196ba4e9
Merge: 7ff008d6d6 620387f294
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Mon Apr 27 15:36:33 2026 +0200

    Merge branch 'main' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 7ff008d6d6
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 24 16:47:38 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 5ad0150fe4
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 24 15:19:32 2026 +0200

    fix page size

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 4f52a79c6a
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 24 14:53:04 2026 +0200

    application refresh

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit a8b8a81375
Merge: 31e7b1dc4b 0459568a96
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 24 13:54:38 2026 +0200

    Merge branch 'main' into rust-proxy

commit 31e7b1dc4b
Merge: 2cb3df2a60 8bf7efecfd
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 23 15:46:53 2026 +0200

    Merge branch 'rust-worker-2' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 8bf7efecfd
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 23 15:33:30 2026 +0200

    fix lint

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit b1ceb28f71
Merge: 1fec16b8e0 39e6c41566
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 23 15:26:14 2026 +0200

    Merge branch 'main' into rust-worker-2

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 2cb3df2a60
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 19:00:42 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 5426881797
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 19:00:26 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 3f703bb21b
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 18:23:54 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit b3c0a50f91
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 16:46:54 2026 +0200

    metrics and logging

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 1fec16b8e0
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 13:40:07 2026 +0200

    run -> start

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 8657d74dc9
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 2 13:22:10 2026 +0200

    root: init rust worker

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 347df15f50
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 14:00:28 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit cf2ed15ced
Merge: dc1d99288f b220e80a0d
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 13:42:43 2026 +0200

    Merge branch 'rust-worker-2' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit b220e80a0d
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 13:40:07 2026 +0200

    run -> start

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 54f6b5c73c
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 2 13:22:10 2026 +0200

    root: init rust worker

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 9fad68bdad
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:12:01 2026 +0200

    packages/ak-common/tracing: get sentry config from API for outposts

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit dc1d99288f
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:51:28 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 8fb795ec89
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:41:40 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit f8f84f5f0b
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:41:33 2026 +0200

    fixup

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 5812558463
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:38:06 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 513462f78d
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:38:02 2026 +0200

    fixup

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 833912b712
Merge: 9fba928666 78a4b06ab3
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:32:31 2026 +0200

    Merge branch 'rust-worker-2' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 78a4b06ab3
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 2 13:22:10 2026 +0200

    root: init rust worker

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit c38e3cbbcf
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:12:01 2026 +0200

    packages/ak-common/tracing: get sentry config from API for outposts

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 9fba928666
Merge: ce8f33416e 668f37ea41
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:16:50 2026 +0200

    Merge branch 'main' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit ce8f33416e
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 16:41:26 2026 +0200

    ws

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 6308ec3360
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Tue Apr 14 15:04:03 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 915bf6942e
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 10 17:16:32 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit e63d2afb29
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 10 14:10:05 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit d103cea26a
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 2 13:22:10 2026 +0200

    root: init rust worker

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-05 15:53:17 +02:00
dependabot[bot]
5357f42029 web: bump vite from 8.0.8 to 8.0.10 in /web (#21842)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.8 to 8.0.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.10
  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>
2026-05-05 14:50:15 +02:00
Dewi Roberts
716bc6e136 api: set authenticated session user agent nullable properties (#22059)
* Set properties to nullable and regenerate schema

* Make gen

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-05-05 14:47:27 +02:00
Dewi Roberts
60355fdf80 web/admin: redirect stage: adds mention of static url (#22060)
Adds mention of static url, not just flow redirect
2026-05-05 14:46:56 +02:00
dependabot[bot]
828a380569 web: bump axios from 1.15.0 to 1.16.0 in /web (#22058)
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 14:25:07 +02:00
Luca Sannitu
b04f8a6177 providers/oauth2: override RedirectURITypeEnum capitalization for generated API (#22037)
* fix(providers/oauth2): correct RedirectURITypeEnum capitalization in API schema

* fix: remove encoding artifacts introduced during client regeneration
2026-05-05 14:18:02 +02:00
Dominic R
ff190847f2 website/docs: document language settings (#21968) 2026-05-05 08:08:00 -04:00
Dominic R
a7339c7f87 website/docs: document supported PostgreSQL versions (#21967) 2026-05-05 08:07:24 -04:00
dependabot[bot]
38ae472f6c website: bump docusaurus-theme-openapi-docs from 5.0.1 to 5.0.2 in /website (#22052)
* website: bump docusaurus-theme-openapi-docs in /website

Bumps [docusaurus-theme-openapi-docs](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/tree/HEAD/packages/docusaurus-theme-openapi-docs) from 5.0.1 to 5.0.2.
- [Release notes](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/releases)
- [Changelog](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/commits/v5.0.2/packages/docusaurus-theme-openapi-docs)

---
updated-dependencies:
- dependency-name: docusaurus-theme-openapi-docs
  dependency-version: 5.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* bump

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-05-05 13:36:39 +02:00
dependabot[bot]
7d0656c6fa web: bump the storybook group across 1 directory with 5 updates (#22024)
Bumps the storybook group with 4 updates in the /web directory: [@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs), [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links), [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) and [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite).


Updates `@storybook/addon-docs` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/docs)

Updates `@storybook/addon-links` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/addons/links)

Updates `@storybook/web-components` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/frameworks/web-components-vite)

Updates `storybook` from 10.3.5 to 10.3.6
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.3.6/code/core)

---
updated-dependencies:
- dependency-name: "@storybook/addon-docs"
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 10.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 13:18:52 +02:00
Teffen Ellis
0bbe415b5b revert: web: Consistent use of "User Dashboard" (#22038) (#22046)
Revert "web: Consistent use of "User Dashboard" (#22038)"

This reverts commit d69433b314.
2026-05-05 13:17:40 +02:00
dependabot[bot]
e52c1b2bdc core: bump metrics-exporter-prometheus from 0.18.1 to 0.18.3 (#22057)
Bumps [metrics-exporter-prometheus](https://github.com/metrics-rs/metrics) from 0.18.1 to 0.18.3.
- [Changelog](https://github.com/metrics-rs/metrics/blob/main/release.toml)
- [Commits](https://github.com/metrics-rs/metrics/compare/metrics-exporter-prometheus-v0.18.1...metrics-exporter-prometheus-v0.18.3)

---
updated-dependencies:
- dependency-name: metrics-exporter-prometheus
  dependency-version: 0.18.3
  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>
2026-05-05 13:09:40 +02:00
authentik-automation[bot]
5064167f28 core, web: update translations (#22047)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-05-05 13:08:29 +02:00
dependabot[bot]
bca0f51b53 core: bump cryptography from 47.0.0 to 48.0.0 (#22053)
Bumps [cryptography](https://github.com/pyca/cryptography) from 47.0.0 to 48.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/47.0.0...48.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 48.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 13:08:25 +02:00
dependabot[bot]
67c197e5a5 core: bump psycopg[c,pool] from 3.3.3 to 3.3.4 (#22054)
Bumps [psycopg[c,pool]](https://github.com/psycopg/psycopg) from 3.3.3 to 3.3.4.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.3...3.3.4)

---
updated-dependencies:
- dependency-name: psycopg[c,pool]
  dependency-version: 3.3.4
  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>
2026-05-05 13:08:21 +02:00
dependabot[bot]
32b17da699 ci: bump taiki-e/install-action from 2.75.28 to 2.75.29 in /.github/actions/setup (#22056)
ci: bump taiki-e/install-action in /.github/actions/setup

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.28 to 2.75.29.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](51cd0b8c04...b5fddbb536)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.29
  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>
2026-05-05 13:08:16 +02:00
Dominic R
c75eed630a web: remove native fieldset borders from action groups (#21334)
* web: remove native fieldset borders from action groups

Refs:\n- https://authentiksecurity.slack.com/archives/C08C0SCU2JV/p1775085687040019\n- https://authentiksecurity.slack.com/archives/C08C0SCU2JV/p1774988472501059

* Use consistent naming.

* Fix up styles, selector specifics, compatibility mode.

* Fix field autocapitalization, keyboard behavior.

* Fix default height.

* Fix for mid-size tablet viewports.

- Helped with debugging on mobile.

* Fix linter warning.

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-05-05 06:17:23 +02:00
Dominic R
9f17d6df96 website/docs: document blueprint import options (#21973)
* website/docs: document blueprint import options

Closes: https://github.com/goauthentik/authentik/issues/21204

* Update website/docs/customize/blueprints/index.mdx

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

* Update website/docs/customize/blueprints/working_with_blueprints.md

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

* Clarify blueprint docs

* Apply suggestions from code review

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-05-05 01:07:23 +00:00
Dominic R
13c8ad5c56 website/integrations: clarify Jellyfin LDAP bind permissions (#21975)
* website/integrations: clarify Jellyfin LDAP bind permissions

Closes: https://github.com/goauthentik/authentik/issues/9770

* website/docs: clarify jellyfin LDAP service account

* website/docs: link jellyfin LDAP setup steps

* Update index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-05-05 00:58:10 +00:00
Marcelo Elizeche Landó
28209c03e2 docs: Improve docs on webauthn authenticator attachment (#22045)
Improve docs on webauthn authenticator attachment
2026-05-05 00:34:54 +00:00
Marcelo Elizeche Landó
f47cf08d8a website/docs: Add docs for webauthn hints feature (#20933)
* Add docs for webauthn hints feature

* remove accidentally added file

* Apply suggestions from code review

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* Apply suggestions from code review

* Apply suggestions from code review

* Apply suggestions from code review

* point to our docs

---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Signed-off-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-05-04 23:47:48 +00:00
289 changed files with 2354 additions and 2227 deletions

View File

@@ -64,7 +64,7 @@ runs:
rustflags: ""
- name: Setup rust dependencies
if: ${{ contains(inputs.dependencies, 'rust') }}
uses: taiki-e/install-action@51cd0b8c0499559d9a4d75c0f5c67bec3a894ec8 # v2
uses: taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2
with:
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
- name: Setup node (web)

127
Cargo.lock generated
View File

@@ -176,10 +176,12 @@ dependencies = [
"arc-swap",
"argh",
"authentik-axum",
"authentik-client",
"authentik-common",
"axum",
"color-eyre",
"eyre",
"futures",
"hyper-unix-socket",
"hyper-util",
"metrics",
@@ -187,9 +189,18 @@ dependencies = [
"nix 0.31.2",
"pyo3",
"pyo3-build-config",
"rand 0.10.1",
"serde",
"serde_json",
"serde_repr",
"sqlx",
"time",
"tokio",
"tokio-retry2",
"tokio-tungstenite",
"tower",
"tracing",
"url",
"uuid",
"which",
]
@@ -542,6 +553,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -779,6 +801,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.4.0"
@@ -1003,6 +1034,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "evmap"
version = "11.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
dependencies = [
"hashbag",
"left-right",
"smallvec",
]
[[package]]
name = "eyre"
version = "0.6.12"
@@ -1219,6 +1261,21 @@ dependencies = [
"slab",
]
[[package]]
name = "generator"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows-link",
"windows-result",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1265,6 +1322,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -1300,6 +1358,12 @@ dependencies = [
"tracing",
]
[[package]]
name = "hashbag"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -1857,6 +1921,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "left-right"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a"
dependencies = [
"crossbeam-utils",
"loom",
"slab",
]
[[package]]
name = "libc"
version = "0.2.183"
@@ -1928,6 +2003,19 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -1977,11 +2065,12 @@ dependencies = [
[[package]]
name = "metrics-exporter-prometheus"
version = "0.18.1"
version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda"
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
dependencies = [
"base64 0.22.1",
"evmap",
"indexmap",
"metrics",
"metrics-util",
@@ -2755,6 +2844,17 @@ dependencies = [
"rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@@ -2793,6 +2893,12 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_xoshiro"
version = "0.7.0"
@@ -3103,6 +3209,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -3356,7 +3468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -3367,7 +3479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -3937,8 +4049,12 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
@@ -4152,8 +4268,11 @@ dependencies = [
"httparse",
"log",
"rand 0.9.2",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 2.0.18",
"url",
]
[[package]]

View File

@@ -44,12 +44,13 @@ hyper-util = "= 0.1.20"
ipnet = { version = "= 2.12.0", features = ["serde"] }
json-subscriber = "= 0.2.8"
metrics = "= 0.24.5"
metrics-exporter-prometheus = { version = "= 0.18.1", default-features = false }
metrics-exporter-prometheus = { version = "= 0.18.3", default-features = false }
nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
notify = "= 8.2.0"
pin-project-lite = "= 0.2.17"
pyo3 = "= 0.28.3"
pyo3-build-config = "= 0.28.3"
rand = "= 0.10.1"
regex = "= 1.12.3"
reqwest = { version = "= 0.13.3", features = [
"form",
@@ -100,6 +101,10 @@ time = { version = "= 0.3.47", features = ["macros"] }
tokio = { version = "= 1.52.1", features = ["full", "tracing"] }
tokio-retry2 = "= 0.9.1"
tokio-rustls = "= 0.26.4"
tokio-tungstenite = { version = "= 0.29.0", features = [
"rustls-tls-webpki-roots",
"url",
] }
tokio-util = { version = "= 0.7.18", features = ["full"] }
tower = "= 0.5.3"
tower-http = { version = "= 0.6.8", features = ["timeout"] }
@@ -260,28 +265,39 @@ publish.workspace = true
[features]
default = ["core", "proxy"]
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
proxy = ["ak-common/proxy"]
proxy = ["ak-common/proxy", "dep:ak-client"]
[build-dependencies]
pyo3-build-config.workspace = true
[dependencies]
ak-axum.workspace = true
ak-client = { workspace = true, optional = true }
ak-common.workspace = true
arc-swap.workspace = true
argh.workspace = true
axum.workspace = true
color-eyre.workspace = true
eyre.workspace = true
futures.workspace = true
hyper-unix-socket.workspace = true
hyper-util.workspace = true
metrics.workspace = true
metrics-exporter-prometheus.workspace = true
metrics.workspace = true
nix.workspace = true
pyo3 = { workspace = true, optional = true }
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_repr.workspace = true
sqlx = { workspace = true, optional = true }
time.workspace = true
tokio-retry2.workspace = true
tokio-tungstenite.workspace = true
tokio.workspace = true
tower.workspace = true
tracing.workspace = true
url.workspace = true
uuid.workspace = true
which.workspace = true

View File

@@ -32,19 +32,19 @@ from authentik.rbac.decorators import permission_required
class UserAgentDeviceDict(TypedDict):
"""User agent device"""
brand: str
brand: str | None = None
family: str
model: str
model: str | None = None
class UserAgentOSDict(TypedDict):
"""User agent os"""
family: str
major: str
minor: str
patch: str
patch_minor: str
major: str | None = None
minor: str | None = None
patch: str | None = None
patch_minor: str | None = None
class UserAgentBrowserDict(TypedDict):

View File

@@ -187,6 +187,7 @@ SPECTACULAR_SETTINGS = {
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
"PromptTypeEnum": "authentik.stages.prompt.models.FieldTypes",
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
"RedirectURITypeEnum": "authentik.providers.oauth2.models.RedirectURIType",
"SAMLBindingsEnum": "authentik.providers.saml.models.SAMLBindings",
"SAMLLogoutMethods": "authentik.providers.saml.models.SAMLLogoutMethods",
"SAMLNameIDPolicyEnum": "authentik.sources.saml.models.SAMLNameIDPolicy",

View File

@@ -33,7 +33,6 @@ class SourceTypeSerializer(PassiveSerializer):
profile_url = CharField(read_only=True, allow_null=True)
oidc_well_known_url = CharField(read_only=True, allow_null=True)
oidc_jwks_url = CharField(read_only=True, allow_null=True)
client_secret_required = BooleanField()
class OAuthSourceSerializer(SourceSerializer):
@@ -66,15 +65,6 @@ class OAuthSourceSerializer(SourceSerializer):
)
source_type = registry.find_type(provider_type_name)
if not source_type.client_secret_required and "consumer_secret" not in attrs:
attrs["consumer_secret"] = ""
if (
source_type.client_secret_required
and not self.instance
and not attrs.get("consumer_secret")
):
raise ValidationError({"consumer_secret": "This field is required."})
well_known = attrs.get("oidc_well_known_url") or source_type.oidc_well_known_url
inferred_oidc_jwks_url = None
@@ -159,7 +149,7 @@ class OAuthSourceSerializer(SourceSerializer):
"authorization_code_auth_method",
]
extra_kwargs = {
"consumer_secret": {"write_only": True, "allow_blank": True, "required": False},
"consumer_secret": {"write_only": True},
"request_token_url": {"allow_blank": True},
"authorization_url": {"allow_blank": True},
"access_token_url": {"allow_blank": True},

View File

@@ -10,7 +10,6 @@ LOGGER = get_logger()
AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.apple",
"authentik.sources.oauth.types.atproto",
"authentik.sources.oauth.types.azure_ad",
"authentik.sources.oauth.types.discord",
"authentik.sources.oauth.types.entra_id",

View File

@@ -271,15 +271,6 @@ class EntraIDOAuthSource(CreatableType, OAuthSource):
verbose_name_plural = _("Entra ID OAuth Sources")
class AtProtoOAuthSource(CreatableType, OAuthSource):
"""Social Login using AT Protocol."""
class Meta:
abstract = True
verbose_name = _("AT Protocol OAuth Source")
verbose_name_plural = _("AT Protocol OAuth Sources")
class OpenIDConnectOAuthSource(CreatableType, OAuthSource):
"""Login using a Generic OpenID-Connect compliant provider."""

View File

@@ -1,284 +0,0 @@
"""AT Protocol OAuth Source tests"""
from urllib.parse import parse_qs, urlparse
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat
from django.test import RequestFactory, SimpleTestCase
from jwt import decode, get_unverified_header
from requests_mock import Mocker
from authentik.sources.oauth.api.source import OAuthSourceSerializer
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.atproto import (
BSKY_AUTHORIZATION_URL_DEFAULT,
BSKY_PAR_URL_DEFAULT,
BSKY_PUBLIC_PROFILE_URL_DEFAULT,
BSKY_TOKEN_URL_DEFAULT,
AtProtoOAuthClient,
AtProtoType,
)
ATPROTO_DID = "did:plc:z72i7hdynmk6r22z27h6tvur"
ATPROTO_PDS = "https://puffball.us-east.host.bsky.network"
ATPROTO_CLIENT_ID = "https://authentik.example/application/o/atproto/client-metadata.json"
ATPROTO_DID_DOCUMENT = {
"id": ATPROTO_DID,
"alsoKnownAs": ["at://bsky.app"],
"service": [
{
"id": "#atproto_pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": ATPROTO_PDS,
}
],
}
ATPROTO_PROFILE = {
"did": ATPROTO_DID,
"handle": "bsky.app",
"displayName": "Bluesky",
}
CUSTOM_ISSUER = "https://auth.example"
CUSTOM_AUTHORIZATION_URL = f"{CUSTOM_ISSUER}/oauth/authorize"
CUSTOM_PAR_URL = f"{CUSTOM_ISSUER}/oauth/par"
CUSTOM_TOKEN_URL = f"{CUSTOM_ISSUER}/oauth/token"
CUSTOM_PROFILE_URL = f"{CUSTOM_ISSUER}/xrpc/app.bsky.actor.getProfile"
def private_key_pem() -> str:
"""Generate an ES256 private key for DPoP tests."""
return (
ec.generate_private_key(ec.SECP256R1())
.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
.decode()
)
class TestTypeAtProto(SimpleTestCase):
"""AT Protocol OAuth Source tests"""
def setUp(self):
self.source = OAuthSource(
name="test",
slug="test",
provider_type="atproto",
consumer_key=ATPROTO_CLIENT_ID,
)
self.factory = RequestFactory()
def get_request(self):
request = self.factory.get("/")
request.session = {}
return request
def get_callback_request(self, issuer: str = "https://bsky.social"):
request = self.factory.get(f"/?state=state&iss={issuer}&code=code")
request.session = {
"authentik/sources/oauth/atproto/test": {
"state": "state",
"code_verifier": "verifier",
"issuer": issuer,
"private_key": private_key_pem(),
"dpop_nonce": "nonce-1",
"login_hint": None,
"expected_did": None,
}
}
return request
def test_enroll_context(self):
"""Test AT Protocol enrollment context."""
ak_context = AtProtoType().get_base_user_properties(
source=self.source,
info=ATPROTO_PROFILE,
)
self.assertEqual(ak_context["username"], ATPROTO_PROFILE["handle"])
self.assertEqual(ak_context["name"], ATPROTO_PROFILE["displayName"])
self.assertIsNone(ak_context["email"])
def test_serializer_allows_missing_secret(self):
"""Test AT Protocol sources can be created without a client secret."""
serializer = OAuthSourceSerializer()
validated = serializer.validate(
{
"name": "test-atproto",
"slug": "test-atproto",
"provider_type": "atproto",
"consumer_key": ATPROTO_CLIENT_ID,
}
)
self.assertEqual(validated["consumer_secret"], "")
@Mocker()
def test_redirect_uses_par_dpop_pkce_and_no_secret(self, mock: Mocker):
"""Test authorization starts with a DPoP-bound pushed authorization request."""
mock.post(
BSKY_PAR_URL_DEFAULT,
json={"request_uri": "urn:request:123"},
headers={"DPoP-Nonce": "nonce-1"},
)
request = self.get_request()
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
redirect_url = client.get_redirect_url({"scope": ["atproto", "transition:generic"]})
parsed_redirect = urlparse(redirect_url)
parsed_query = parse_qs(parsed_redirect.query)
parsed_redirect_url = (
f"{parsed_redirect.scheme}://{parsed_redirect.netloc}{parsed_redirect.path}"
)
self.assertEqual(parsed_redirect_url, BSKY_AUTHORIZATION_URL_DEFAULT)
self.assertEqual(parsed_query["client_id"], [ATPROTO_CLIENT_ID])
self.assertEqual(parsed_query["request_uri"], ["urn:request:123"])
self.assertEqual(len(mock.request_history), 1)
par_request = mock.request_history[0]
self.assertIn("DPoP", par_request.headers)
self.assertEqual(par_request.text.count("client_secret"), 0)
self.assertIn("client_id=https%3A%2F%2Fauthentik.example", par_request.text)
self.assertIn("code_challenge_method=S256", par_request.text)
self.assertIn("scope=atproto+transition%3Ageneric", par_request.text)
header = get_unverified_header(par_request.headers["DPoP"])
payload = decode(par_request.headers["DPoP"], options={"verify_signature": False})
self.assertEqual(header["typ"], "dpop+jwt")
self.assertEqual(header["alg"], "ES256")
self.assertEqual(payload["htm"], "POST")
self.assertEqual(payload["htu"], BSKY_PAR_URL_DEFAULT)
@Mocker()
def test_custom_urls_override_bluesky_defaults(self, mock: Mocker):
"""Test non-Bluesky AT Protocol endpoint configuration."""
source = OAuthSource(
name="test",
slug="test",
provider_type="atproto",
consumer_key=ATPROTO_CLIENT_ID,
authorization_url=CUSTOM_AUTHORIZATION_URL,
request_token_url=CUSTOM_PAR_URL,
access_token_url=CUSTOM_TOKEN_URL,
profile_url=CUSTOM_PROFILE_URL,
)
mock.post(
CUSTOM_PAR_URL,
json={"request_uri": "urn:request:custom"},
headers={"DPoP-Nonce": "nonce-custom"},
)
request = self.get_request()
client = AtProtoOAuthClient(source, request, callback="/callback/")
redirect_url = client.get_redirect_url({"scope": ["atproto"]})
parsed_redirect = urlparse(redirect_url)
self.assertEqual(
f"{parsed_redirect.scheme}://{parsed_redirect.netloc}{parsed_redirect.path}",
CUSTOM_AUTHORIZATION_URL,
)
self.assertEqual(request.session[client.session_key]["issuer"], CUSTOM_ISSUER)
self.assertEqual(mock.request_history[0].url, CUSTOM_PAR_URL)
@Mocker()
def test_access_token_validates_subject_scope_and_issuer(self, mock: Mocker):
"""Test callback token response validation."""
mock.post(
BSKY_TOKEN_URL_DEFAULT,
json={
"access_token": "access",
"refresh_token": "refresh",
"token_type": "DPoP",
"expires_in": 300,
"sub": ATPROTO_DID,
"scope": "atproto transition:generic",
},
headers={"DPoP-Nonce": "nonce-2"},
)
mock.get(f"https://plc.directory/{ATPROTO_DID}", json=ATPROTO_DID_DOCUMENT)
mock.get(
f"{ATPROTO_PDS}/.well-known/oauth-protected-resource",
json={"authorization_servers": ["https://bsky.social"]},
)
request = self.get_callback_request()
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
token = client.get_access_token()
self.assertEqual(token["sub"], ATPROTO_DID)
self.assertEqual(token["pds_url"], ATPROTO_PDS)
token_request = mock.request_history[0]
self.assertIn("DPoP", token_request.headers)
self.assertEqual(token_request.text.count("client_secret"), 0)
self.assertIn("code_verifier=verifier", token_request.text)
@Mocker()
def test_access_token_rejects_non_dpop_token_type(self, mock: Mocker):
"""Test callback rejects token responses that are not DPoP-bound."""
mock.post(
BSKY_TOKEN_URL_DEFAULT,
json={
"access_token": "access",
"token_type": "Bearer",
"sub": ATPROTO_DID,
"scope": "atproto",
},
headers={"DPoP-Nonce": "nonce-2"},
)
client = AtProtoOAuthClient(self.source, self.get_callback_request(), callback="/callback/")
token = client.get_access_token()
self.assertEqual(token["error"], "Token response did not include a DPoP token type.")
@Mocker()
def test_did_web_localhost_uses_http_for_local_testing(self, mock: Mocker):
"""Test did:web localhost resolution for the local AT Protocol simulator."""
mock.get("http://localhost:8787/.well-known/did.json", json={"id": "did:web:localhost"})
client = AtProtoOAuthClient(self.source, self.get_request(), callback="/callback/")
document = client.get_did_document("did:web:localhost%3A8787")
self.assertEqual(document["id"], "did:web:localhost")
@Mocker()
def test_profile_info(self, mock: Mocker):
"""Test public Bluesky profile lookup."""
mock.get(BSKY_PUBLIC_PROFILE_URL_DEFAULT, json=ATPROTO_PROFILE)
client = AtProtoOAuthClient(self.source, self.get_request(), callback="/callback/")
profile = client.get_profile_info({"sub": ATPROTO_DID})
self.assertEqual(profile["did"], ATPROTO_DID)
self.assertEqual(profile["handle"], "bsky.app")
@Mocker()
def test_profile_info_with_transition_email(self, mock: Mocker):
"""Test private session email lookup when transition:email is granted."""
mock.get(BSKY_PUBLIC_PROFILE_URL_DEFAULT, json=ATPROTO_PROFILE)
mock.get(
f"{ATPROTO_PDS}/xrpc/com.atproto.server.getSession",
json={"email": "user@example.com", "emailConfirmed": True},
headers={"DPoP-Nonce": "nonce-3"},
)
request = self.get_request()
request.session = {
"authentik/sources/oauth/atproto/test": {
"state": "state",
"code_verifier": "verifier",
"issuer": "https://bsky.social",
"private_key": private_key_pem(),
"dpop_nonce": "nonce-2",
"login_hint": None,
"expected_did": None,
}
}
client = AtProtoOAuthClient(self.source, request, callback="/callback/")
profile = client.get_profile_info(
{
"sub": ATPROTO_DID,
"scope": "atproto transition:email",
"access_token": "access",
"pds_url": ATPROTO_PDS,
}
)
self.assertEqual(profile["email"], "user@example.com")
session_request = mock.request_history[1]
self.assertEqual(session_request.headers["Authorization"], "DPoP access")
payload = decode(session_request.headers["DPoP"], options={"verify_signature": False})
self.assertIn("ath", payload)

View File

@@ -1,486 +0,0 @@
"""AT Protocol OAuth Views"""
from time import time
from typing import Any
from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse, urlunparse
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.hashes import SHA256, Hash
from cryptography.hazmat.primitives.serialization import (
Encoding,
NoEncryption,
PrivateFormat,
load_pem_private_key,
)
from django.templatetags.static import static
from django.urls import reverse
from django.utils.crypto import constant_time_compare, get_random_string
from jwt import encode
from jwt.algorithms import ECAlgorithm
from jwt.utils import base64url_encode
from requests.exceptions import RequestException
from structlog.stdlib import get_logger
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.utils import pkce_s256_challenge
from authentik.sources.oauth.clients.base import BaseOAuthClient
from authentik.sources.oauth.models import OAuthSource, PKCEMethod
from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
LOGGER = get_logger()
# Bluesky defaults. AT Protocol OAuth requires these endpoint roles, but
# non-Bluesky deployments can use different hosts through the source URL fields.
BSKY_AUTHORIZATION_URL_DEFAULT = "https://bsky.social/oauth/authorize"
BSKY_TOKEN_URL_DEFAULT = "https://bsky.social/oauth/token" # nosec
BSKY_PAR_URL_DEFAULT = "https://bsky.social/oauth/par"
BSKY_ISSUER_DEFAULT = "https://bsky.social"
BSKY_PUBLIC_PROFILE_URL_DEFAULT = "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile"
HTTP_STATUS_BAD_REQUEST = 400
SESSION_KEY_ATPROTO = "authentik/sources/oauth/atproto"
class AtProtoOAuthClient(BaseOAuthClient):
"""AT Protocol OAuth client.
AT Protocol looks like OAuth2 from a distance, but the required security
profile is different enough that sharing the generic OAuth2 client would
hide important behavior: PAR is mandatory, access tokens are DPoP-bound,
public clients use metadata URLs instead of secrets, and the token subject
is the user's DID rather than an OIDC userinfo subject.
"""
def get_client_id(self) -> str:
"""Return the public client metadata URL."""
return self.source.consumer_key
@property
def session_key(self) -> str:
return f"{SESSION_KEY_ATPROTO}/{self.source.slug}"
def get_authorization_url(self) -> str:
if self.source.source_type.urls_customizable and self.source.authorization_url:
return self.source.authorization_url
return self.source.source_type.authorization_url or BSKY_AUTHORIZATION_URL_DEFAULT
def get_token_url(self) -> str:
if self.source.source_type.urls_customizable and self.source.access_token_url:
return self.source.access_token_url
return self.source.source_type.access_token_url or BSKY_TOKEN_URL_DEFAULT
def get_par_url(self) -> str:
if self.source.source_type.urls_customizable and self.source.request_token_url:
return self.source.request_token_url
return self.source.source_type.request_token_url or BSKY_PAR_URL_DEFAULT
def get_issuer(self) -> str:
parsed_url = urlparse(self.get_authorization_url())
if parsed_url.scheme and parsed_url.netloc:
return f"{parsed_url.scheme}://{parsed_url.netloc}"
return BSKY_ISSUER_DEFAULT
def get_redirect_args(self) -> dict[str, str]:
"""AT Protocol redirects are built from PAR responses instead."""
raise NotImplementedError
def get_redirect_url(self, parameters=None):
"""Create a PAR request and redirect with request_uri."""
request_uri = self.create_pushed_authorization_request(parameters or {})
parsed_url = urlparse(self.get_authorization_url())
parsed_args = parse_qs(parsed_url.query)
args = {
"client_id": self.get_client_id(),
"request_uri": request_uri,
}
args.update(parsed_args)
params = urlencode(args, quote_via=quote, doseq=True)
return urlunparse(parsed_url._replace(query=params))
def create_pushed_authorization_request(self, parameters: dict[str, Any]) -> str:
"""Create the pushed authorization request and persist session data."""
state = get_random_string(32)
code_verifier = generate_id(length=128)
private_key = ec.generate_private_key(ec.SECP256R1())
login_hint = parameters.pop("login_hint", None)
scope = parameters.pop("scope", [])
if isinstance(scope, str):
scopes = scope.split()
else:
scopes = list(scope)
if "atproto" not in scopes:
scopes.append("atproto")
# The DPoP key and PKCE verifier must survive the browser redirect so
# the callback can prove it is the same client that created the PAR.
session_data = {
"state": state,
"code_verifier": code_verifier,
"issuer": self.get_issuer(),
"private_key": private_key.private_bytes(
Encoding.PEM,
PrivateFormat.PKCS8,
NoEncryption(),
).decode(),
"dpop_nonce": None,
"login_hint": login_hint,
"expected_did": self.resolve_identifier(login_hint) if login_hint else None,
}
self.request.session[self.session_key] = session_data
# AT Protocol starts the browser flow with a PAR request. The browser
# only receives a request_uri, not the full authorization parameters.
body = {
"client_id": self.get_client_id(),
"response_type": "code",
"redirect_uri": self.request.build_absolute_uri(self.callback),
"scope": " ".join(sorted(set(scopes))),
"state": state,
"code_challenge": pkce_s256_challenge(code_verifier),
"code_challenge_method": PKCEMethod.S256,
}
if login_hint:
body["login_hint"] = login_hint
body.update(parameters)
response = self.request_with_dpop("post", self.get_par_url(), data=body)
try:
request_uri = response.json().get("request_uri")
except ValueError as exc:
raise RequestException("PAR response was not valid JSON", response=response) from exc
if not request_uri:
raise RequestException("PAR response did not include request_uri", response=response)
return request_uri
def get_access_token(self, **request_kwargs) -> dict[str, Any] | None:
"""Fetch the initial access token from the callback code."""
session_data = self.request.session.get(self.session_key)
if not session_data:
LOGGER.warning("No AT Protocol OAuth session found")
return {"error": "No AT Protocol OAuth session found."}
if not constant_time_compare(session_data["state"], self.get_request_arg("state", "")):
LOGGER.warning("AT Protocol OAuth state check failed")
return {"error": "State check failed."}
issuer = self.get_request_arg("iss")
if not issuer or not constant_time_compare(session_data["issuer"], issuer):
LOGGER.warning("AT Protocol OAuth issuer check failed", issuer=issuer)
return {"error": "Issuer check failed."}
code = self.get_request_arg("code")
if not code:
return {"error": self.get_request_arg("error_description") or "No token received."}
data = {
"grant_type": "authorization_code",
"client_id": self.get_client_id(),
"redirect_uri": self.request.build_absolute_uri(self.callback),
"code": code,
"code_verifier": session_data["code_verifier"],
}
try:
response = self.request_with_dpop("post", self.get_token_url(), data=data)
token = response.json()
except ValueError as exc:
LOGGER.warning("AT Protocol token response was not valid JSON", exc=exc)
return None
except RequestException as exc:
LOGGER.warning(
"Unable to fetch AT Protocol access token",
exc=exc,
response=exc.response.text if exc.response is not None else str(exc),
)
return None
validation_error = self.validate_token_response(token, session_data, issuer)
if validation_error:
return {"error": validation_error}
return token
def validate_token_response(
self,
token: dict[str, Any],
session_data: dict[str, Any],
issuer: str,
) -> str | None:
"""Validate AT Protocol token claims and attach the verified PDS URL."""
# The token response identifies the account by DID. That DID becomes
# the stable source connection identifier in authentik.
did = token.get("sub")
if not did:
return "Token response did not include an account DID."
if "atproto" not in token.get("scope", "").split():
return "Token response did not include the atproto scope."
if token.get("token_type") != "DPoP":
return "Token response did not include a DPoP token type."
expected_did = session_data.get("expected_did")
if expected_did and not constant_time_compare(expected_did, did):
LOGGER.warning("AT Protocol OAuth subject check failed", expected=expected_did, did=did)
return "Subject check failed."
# Verify the DID document's PDS points back to the authorization server
# that issued the callback, otherwise a token could claim another DID.
pds_url = self.get_pds_url_for_subject(did, issuer)
if not pds_url:
return "Issuer is not authoritative for this account."
token["pds_url"] = pds_url
return None
def get_profile_info(self, token: dict[str, Any]) -> dict[str, Any] | None:
"""Fetch public profile data for the authenticated DID."""
did = token.get("sub")
if not did:
return None
profile_url = BSKY_PUBLIC_PROFILE_URL_DEFAULT
if self.source.source_type.urls_customizable and self.source.profile_url:
profile_url = self.source.profile_url
response = self.session.get(profile_url, params={"actor": did})
try:
response.raise_for_status()
except RequestException as exc:
LOGGER.warning(
"Unable to fetch AT Protocol profile",
exc=exc,
response=exc.response.text if exc.response is not None else str(exc),
)
return {"did": did}
profile = response.json()
profile["did"] = did
if "transition:email" in token.get("scope", "").split() and token.get("pds_url"):
profile.update(self.get_session_info(token))
return profile
def request_with_dpop(self, method: str, url: str, **kwargs):
"""Make a DPoP request, retrying once when the server provides a fresh nonce."""
response = self.do_dpop_request(method, url, **kwargs)
if response.status_code == HTTP_STATUS_BAD_REQUEST and response.headers.get("DPoP-Nonce"):
self.update_dpop_nonce(response.headers["DPoP-Nonce"])
response = self.do_dpop_request(method, url, **kwargs)
response.raise_for_status()
nonce = response.headers.get("DPoP-Nonce")
if not nonce:
raise RequestException("DPoP response did not include DPoP-Nonce", response=response)
self.update_dpop_nonce(nonce)
return response
def get_session_info(self, token: dict[str, Any]) -> dict[str, Any]:
"""Fetch private session data when transition:email was granted."""
pds_url = token["pds_url"].rstrip("/")
session_url = f"{pds_url}/xrpc/com.atproto.server.getSession"
headers = {
"Authorization": f"DPoP {token['access_token']}",
}
response = self.do_dpop_request(
"get",
session_url,
headers=headers,
access_token=token["access_token"],
)
if response.status_code == HTTP_STATUS_BAD_REQUEST and response.headers.get("DPoP-Nonce"):
self.update_dpop_nonce(response.headers["DPoP-Nonce"])
response = self.do_dpop_request(
"get",
session_url,
headers=headers,
access_token=token["access_token"],
)
try:
response.raise_for_status()
except RequestException as exc:
LOGGER.warning(
"Unable to fetch AT Protocol session info",
exc=exc,
response=exc.response.text if exc.response is not None else str(exc),
)
return {}
nonce = response.headers.get("DPoP-Nonce")
if nonce:
self.update_dpop_nonce(nonce)
try:
return response.json()
except ValueError as exc:
LOGGER.warning("AT Protocol session response was not valid JSON", exc=exc)
return {}
def do_dpop_request(self, method: str, url: str, **kwargs):
access_token = kwargs.pop("access_token", None)
headers = dict(kwargs.pop("headers", {}))
headers["Accept"] = "application/json"
headers["DPoP"] = self.build_dpop_proof(method, url, access_token)
return self.session.request(method, url, headers=headers, **kwargs)
def build_dpop_proof(self, method: str, url: str, access_token: str | None = None) -> str:
session_data = self.request.session[self.session_key]
private_key = load_pem_private_key(session_data["private_key"].encode(), password=None)
if not isinstance(private_key, EllipticCurvePrivateKey):
raise TypeError("DPoP private key must be an EC key")
payload = {
"jti": generate_id(),
"htm": method.upper(),
"htu": url,
"iat": int(time()),
}
if session_data.get("dpop_nonce"):
payload["nonce"] = session_data["dpop_nonce"]
if access_token:
# Resource requests bind the proof to the access token with ath.
digest = Hash(SHA256())
digest.update(access_token.encode())
payload["ath"] = base64url_encode(digest.finalize()).decode()
public_jwk = ECAlgorithm.to_jwk(private_key.public_key(), as_dict=True)
public_jwk.pop("kid", None)
return encode(
payload,
private_key,
algorithm="ES256",
headers={
"typ": "dpop+jwt",
"jwk": public_jwk,
},
)
def update_dpop_nonce(self, nonce: str) -> None:
session_data = self.request.session[self.session_key]
session_data["dpop_nonce"] = nonce
self.request.session[self.session_key] = session_data
def get_request_arg(self, key: str, default: Any | None = None) -> Any:
if self.request.method == "POST":
return self.request.POST.get(key, default)
return self.request.GET.get(key, default)
def resolve_identifier(self, identifier: str | None) -> str | None:
"""Resolve a handle or DID to a DID."""
if not identifier:
return None
if identifier.startswith("did:"):
return identifier
response = self.session.get(
f"{self.get_issuer()}/xrpc/com.atproto.identity.resolveHandle",
params={"handle": identifier.removeprefix("@")},
)
try:
response.raise_for_status()
except RequestException as exc:
LOGGER.warning(
"Unable to resolve AT Protocol login hint",
identifier=identifier,
exc=exc,
)
return None
try:
return response.json().get("did")
except ValueError as exc:
LOGGER.warning("AT Protocol handle resolution response was not valid JSON", exc=exc)
return None
def get_pds_url_for_subject(self, did: str, issuer: str) -> str | None:
"""Verify that the DID's PDS resolves to the callback issuer."""
try:
did_document = self.get_did_document(did)
pds_url = self.get_pds_url(did_document)
if not pds_url:
LOGGER.warning("DID document does not include an atproto PDS", did=did)
return None
resource_metadata = self.session.get(
f"{pds_url.rstrip('/')}/.well-known/oauth-protected-resource"
)
resource_metadata.raise_for_status()
try:
authorization_servers = resource_metadata.json().get("authorization_servers", [])
except ValueError as exc:
raise RequestException(
"OAuth protected resource metadata was not valid JSON",
response=resource_metadata,
) from exc
except RequestException as exc:
LOGGER.warning("Unable to verify AT Protocol issuer", did=did, issuer=issuer, exc=exc)
return None
if issuer in authorization_servers:
return pds_url
return None
def get_did_document(self, did: str) -> dict[str, Any]:
if did.startswith("did:plc:"):
response = self.session.get(f"https://plc.directory/{did}")
elif did.startswith("did:web:"):
# did:web resolves by fetching a DID document from the hostname in the DID.
# The AT Protocol local simulator uses did:web:localhost, which cannot use
# HTTPS locally; real did:web identities should resolve over HTTPS.
did_parts = [unquote(part) for part in did.removeprefix("did:web:").split(":")]
host = did_parts[0]
path = "/".join(did_parts[1:])
scheme = "http" if host.startswith(("localhost", "127.0.0.1")) else "https"
did_path = f"{path}/did.json" if path else ".well-known/did.json"
response = self.session.get(f"{scheme}://{host}/{did_path}")
else:
raise RequestException(f"Unsupported DID method: {did}")
response.raise_for_status()
try:
return response.json()
except ValueError as exc:
raise RequestException("DID document was not valid JSON", response=response) from exc
def get_pds_url(self, did_document: dict[str, Any]) -> str | None:
for service in did_document.get("service", []):
if service.get("id") == "#atproto_pds":
return service.get("serviceEndpoint")
if service.get("type") == "AtprotoPersonalDataServer":
return service.get("serviceEndpoint")
return None
class AtProtoOAuthRedirect(OAuthRedirect):
"""AT Protocol OAuth redirect."""
client_class = AtProtoOAuthClient
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return {
"scope": ["atproto"],
}
class AtProtoOAuthCallback(OAuthCallback):
"""AT Protocol OAuth callback."""
client_class = AtProtoOAuthClient
def get_callback_url(self, source: OAuthSource) -> str:
return reverse(
"authentik_sources_oauth:oauth-client-callback",
kwargs={"source_slug": source.slug},
)
def get_user_id(self, info: dict[str, Any]) -> str | None:
return info.get("did")
@registry.register()
class AtProtoType(SourceType):
"""AT Protocol Type definition"""
callback_view = AtProtoOAuthCallback
redirect_view = AtProtoOAuthRedirect
verbose_name = "AT Protocol"
name = "atproto"
# Defaults target Bluesky. They are editable because other AT Protocol
# authorization servers can expose the same endpoint roles on different URLs.
authorization_url = BSKY_AUTHORIZATION_URL_DEFAULT
request_token_url = BSKY_PAR_URL_DEFAULT
access_token_url = BSKY_TOKEN_URL_DEFAULT
profile_url = BSKY_PUBLIC_PROFILE_URL_DEFAULT
urls_customizable = True
pkce = PKCEMethod.S256
client_secret_required = False
def icon_url(self) -> str:
return static("authentik/sources/atproto.svg")
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
return {
"username": info.get("handle") or info.get("did"),
"email": info.get("email"),
"name": info.get("displayName") or info.get("handle"),
}

View File

@@ -42,8 +42,6 @@ class SourceType:
oidc_jwks_url: str | None = None
pkce: PKCEMethod = PKCEMethod.NONE
client_secret_required = True
authorization_code_auth_method: AuthorizationCodeAuthMethod = (
AuthorizationCodeAuthMethod.BASIC_AUTH
)

View File

@@ -16,7 +16,7 @@ class RedirectMode(models.TextChoices):
class RedirectStage(Stage):
"""Redirect the user to another flow, potentially with all gathered context."""
"""Redirect the user to a static URL or another flow, optionally with all gathered context."""
keep_context = models.BooleanField(default=True)
mode = models.TextField(choices=RedirectMode.choices)

View File

@@ -12967,7 +12967,6 @@
"type": "string",
"enum": [
"apple",
"atproto",
"openidconnect",
"entraid",
"azuread",
@@ -13039,6 +13038,7 @@
},
"consumer_secret": {
"type": "string",
"minLength": 1,
"title": "Consumer secret"
},
"additional_scopes": {

View File

@@ -101,8 +101,6 @@ RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
rustc --version && \
cargo --version
RUN cat /root/.rustup/settings.toml
# Stage: Download uv
FROM ghcr.io/astral-sh/uv:0.11.5@sha256:555ac94f9a22e656fc5f2ce5dfee13b04e94d099e46bb8dd3a73ec7263f2e484 AS uv
# Stage: Base python image

View File

@@ -21,33 +21,45 @@ COPY web .
RUN npm run build-proxy
# Stage 2: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.26.2-trixie@sha256:4a7137ea573f79c86ae451ff05817ed762ef5597fcf732259e97abeb3108d873 AS builder
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ARG GOOS=$TARGETOS
ARG GOARCH=$TARGETARCH
WORKDIR /go/src/goauthentik.io
ENV PATH="/root/.cargo/bin:$PATH"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
dpkg --add-architecture arm64 && \
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
apt-get update && \
apt-get install -y --no-install-recommends crossbuild-essential-arm64 gcc-aarch64-linux-gnu
# Required for installing pip packages
apt-get install -y --no-install-recommends \
# Build essentials
build-essential \
# aws-lc deps
cmake clang golang && \
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain none && \
rustup install && \
rustup default "$(sed -n 's/channel = "\(.*\)"/\1/p' rust-toolchain.toml)" && \
rustc --version && \
cargo --version
# See https://github.com/aws/aws-lc-rs/issues/569
ENV AWS_LC_FIPS_SYS_CC=clang
RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
--mount=type=bind,target=/go/src/goauthentik.io/go.sum,src=./go.sum \
--mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
go build -o /go/proxy ./cmd/proxy
RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
--mount=type=bind,target=Cargo.toml,src=Cargo.toml \
--mount=type=bind,target=Cargo.lock,src=Cargo.lock \
--mount=type=bind,target=.cargo/,src=.cargo/ \
--mount=type=bind,target=src/,src=src/ \
--mount=type=bind,target=packages/,src=packages/ \
--mount=type=bind,target=authentik/lib/default.yml,src=authentik/lib/default.yml \
# Required otherwise workspace discovery fails
--mount=type=bind,target=website/scripts/docsmg/,src=website/scripts/docsmg/ \
--mount=type=cache,id=cargo-git-db-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/git/db/ \
--mount=type=cache,id=cargo-registry-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/registry/ \
--mount=type=cache,id=rust-target-$TARGETARCH$TARGETVARIANT,target=/build/target/ \
cargo build --package authentik --no-default-features --features proxy --locked --release && \
cp ./target/release/authentik /bin/authentik
# Stage 3: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341
@@ -72,13 +84,13 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/*
COPY --from=builder /go/proxy /
COPY --from=builder /bin/authentik /
COPY --from=web-builder /static/robots.txt /web/robots.txt
COPY --from=web-builder /static/security.txt /web/security.txt
COPY --from=web-builder /static/dist/ /web/dist/
COPY --from=web-builder /static/authentik/ /web/authentik/
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/proxy", "healthcheck" ]
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/authentik", "healthcheck" ]
EXPOSE 9000 9300 9443
@@ -87,4 +99,4 @@ USER 1000
ENV TMPDIR=/dev/shm/ \
GOFIPS=1
ENTRYPOINT ["/proxy"]
ENTRYPOINT ["/authentik", "proxy"]

Binary file not shown.

View File

@@ -1,6 +1,6 @@
//! Utilities for working with the authentik API client.
use ak_client::apis::configuration::Configuration;
use ak_client::{apis::configuration::Configuration, models::Pagination};
use eyre::{Result, eyre};
use url::Url;
@@ -60,6 +60,42 @@ pub fn make_config() -> Result<Configuration> {
})
}
/// Fetch all pages from a paginated API endpoint, returning all results combined.
///
/// - `fetch`: a function that takes a page number and returns a future resolving to a paginated
/// response.
/// - `get_pagination`: a function that extracts the [`Pagination`] metadata from a response.
/// - `get_results`: a function that extracts the result items from a response.
pub async fn fetch_all<T, R, E, F, Fut>(
fetch: F,
get_pagination: impl Fn(&R) -> &Pagination,
get_results: impl Fn(R) -> Vec<T>,
) -> std::result::Result<Vec<T>, E>
where
F: Fn(i32) -> Fut,
Fut: Future<Output = std::result::Result<R, E>>,
{
let mut page = 1;
let mut results = Vec::with_capacity(0);
loop {
let response = fetch(page).await?;
let next = get_pagination(&response).next;
if page == 1 {
let count = get_pagination(&response).count as usize;
results.reserve(count);
}
results.extend(get_results(response));
if next > 0.0 {
page += 1;
} else {
break;
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use serde_json::json;

View File

@@ -30,12 +30,12 @@ pub fn install() -> Result<()> {
}
if config.debug {
let console_layer = console_subscriber::ConsoleLayer::builder()
.server_addr(config.listen.debug_tokio)
.spawn();
// let console_layer = console_subscriber::ConsoleLayer::builder()
// .server_addr(config.listen.debug_tokio)
// .spawn();
tracing_subscriber::registry()
.with(ErrorLayer::default())
.with(console_layer)
// .with(console_layer)
.with(
fmt::layer()
.compact()
@@ -186,12 +186,9 @@ pub mod sentry {
sentry_dsn: Some(config.sentry_dsn),
environment: config.environment,
send_pii: config.send_pii,
#[expect(
clippy::cast_possible_truncation,
reason = "This is fine, we'll never get big values here."
)]
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "This is fine, we'll never get big values here."
)]
sample_rate: config.traces_sample_rate as f32,

View File

@@ -23,7 +23,7 @@ export interface AuthenticatedSessionUserAgentDevice {
* @type {string}
* @memberof AuthenticatedSessionUserAgentDevice
*/
brand: string;
brand: string | null;
/**
*
* @type {string}
@@ -35,7 +35,7 @@ export interface AuthenticatedSessionUserAgentDevice {
* @type {string}
* @memberof AuthenticatedSessionUserAgentDevice
*/
model: string;
model: string | null;
}
/**

View File

@@ -29,25 +29,25 @@ export interface AuthenticatedSessionUserAgentOs {
* @type {string}
* @memberof AuthenticatedSessionUserAgentOs
*/
major: string;
major: string | null;
/**
*
* @type {string}
* @memberof AuthenticatedSessionUserAgentOs
*/
minor: string;
minor: string | null;
/**
*
* @type {string}
* @memberof AuthenticatedSessionUserAgentOs
*/
patch: string;
patch: string | null;
/**
*
* @type {string}
* @memberof AuthenticatedSessionUserAgentOs
*/
patchMinor: string;
patchMinor: string | null;
}
/**

View File

@@ -162,7 +162,7 @@ export interface OAuthSourceRequest {
* @type {string}
* @memberof OAuthSourceRequest
*/
consumerSecret?: string;
consumerSecret: string;
/**
*
* @type {string}
@@ -203,6 +203,7 @@ export function instanceOfOAuthSourceRequest(value: object): value is OAuthSourc
if (!("slug" in value) || value["slug"] === undefined) return false;
if (!("providerType" in value) || value["providerType"] === undefined) return false;
if (!("consumerKey" in value) || value["consumerKey"] === undefined) return false;
if (!("consumerSecret" in value) || value["consumerSecret"] === undefined) return false;
return true;
}
@@ -251,7 +252,7 @@ export function OAuthSourceRequestFromJSONTyped(
profileUrl: json["profile_url"] == null ? undefined : json["profile_url"],
pkce: json["pkce"] == null ? undefined : PKCEMethodEnumFromJSON(json["pkce"]),
consumerKey: json["consumer_key"],
consumerSecret: json["consumer_secret"] == null ? undefined : json["consumer_secret"],
consumerSecret: json["consumer_secret"],
additionalScopes: json["additional_scopes"] == null ? undefined : json["additional_scopes"],
oidcWellKnownUrl:
json["oidc_well_known_url"] == null ? undefined : json["oidc_well_known_url"],

View File

@@ -18,7 +18,6 @@
*/
export const ProviderTypeEnum = {
Apple: "apple",
Atproto: "atproto",
Openidconnect: "openidconnect",
Entraid: "entraid",
Azuread: "azuread",

View File

@@ -14,8 +14,8 @@
import type { MatchingModeEnum } from "./MatchingModeEnum";
import { MatchingModeEnumFromJSON, MatchingModeEnumToJSON } from "./MatchingModeEnum";
import type { RedirectUriTypeEnum } from "./RedirectUriTypeEnum";
import { RedirectUriTypeEnumFromJSON, RedirectUriTypeEnumToJSON } from "./RedirectUriTypeEnum";
import type { RedirectURITypeEnum } from "./RedirectURITypeEnum";
import { RedirectURITypeEnumFromJSON, RedirectURITypeEnumToJSON } from "./RedirectURITypeEnum";
/**
* A single allowed redirect URI entry
@@ -37,10 +37,10 @@ export interface RedirectURI {
url: string;
/**
*
* @type {RedirectUriTypeEnum}
* @type {RedirectURITypeEnum}
* @memberof RedirectURI
*/
redirectUriType?: RedirectUriTypeEnum;
redirectUriType?: RedirectURITypeEnum;
}
/**
@@ -66,7 +66,7 @@ export function RedirectURIFromJSONTyped(json: any, ignoreDiscriminator: boolean
redirectUriType:
json["redirect_uri_type"] == null
? undefined
: RedirectUriTypeEnumFromJSON(json["redirect_uri_type"]),
: RedirectURITypeEnumFromJSON(json["redirect_uri_type"]),
};
}
@@ -85,6 +85,6 @@ export function RedirectURIToJSONTyped(
return {
matching_mode: MatchingModeEnumToJSON(value["matchingMode"]),
url: value["url"],
redirect_uri_type: RedirectUriTypeEnumToJSON(value["redirectUriType"]),
redirect_uri_type: RedirectURITypeEnumToJSON(value["redirectUriType"]),
};
}

View File

@@ -14,8 +14,8 @@
import type { MatchingModeEnum } from "./MatchingModeEnum";
import { MatchingModeEnumFromJSON, MatchingModeEnumToJSON } from "./MatchingModeEnum";
import type { RedirectUriTypeEnum } from "./RedirectUriTypeEnum";
import { RedirectUriTypeEnumFromJSON, RedirectUriTypeEnumToJSON } from "./RedirectUriTypeEnum";
import type { RedirectURITypeEnum } from "./RedirectURITypeEnum";
import { RedirectURITypeEnumFromJSON, RedirectURITypeEnumToJSON } from "./RedirectURITypeEnum";
/**
* A single allowed redirect URI entry
@@ -37,10 +37,10 @@ export interface RedirectURIRequest {
url: string;
/**
*
* @type {RedirectUriTypeEnum}
* @type {RedirectURITypeEnum}
* @memberof RedirectURIRequest
*/
redirectUriType?: RedirectUriTypeEnum;
redirectUriType?: RedirectURITypeEnum;
}
/**
@@ -69,7 +69,7 @@ export function RedirectURIRequestFromJSONTyped(
redirectUriType:
json["redirect_uri_type"] == null
? undefined
: RedirectUriTypeEnumFromJSON(json["redirect_uri_type"]),
: RedirectURITypeEnumFromJSON(json["redirect_uri_type"]),
};
}
@@ -88,6 +88,6 @@ export function RedirectURIRequestToJSONTyped(
return {
matching_mode: MatchingModeEnumToJSON(value["matchingMode"]),
url: value["url"],
redirect_uri_type: RedirectUriTypeEnumToJSON(value["redirectUriType"]),
redirect_uri_type: RedirectURITypeEnumToJSON(value["redirectUriType"]),
};
}

View File

@@ -0,0 +1,57 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const RedirectURITypeEnum = {
Authorization: "authorization",
Logout: "logout",
UnknownDefaultOpenApi: "11184809",
} as const;
export type RedirectURITypeEnum = (typeof RedirectURITypeEnum)[keyof typeof RedirectURITypeEnum];
export function instanceOfRedirectURITypeEnum(value: any): boolean {
for (const key in RedirectURITypeEnum) {
if (Object.prototype.hasOwnProperty.call(RedirectURITypeEnum, key)) {
if (RedirectURITypeEnum[key as keyof typeof RedirectURITypeEnum] === value) {
return true;
}
}
}
return false;
}
export function RedirectURITypeEnumFromJSON(json: any): RedirectURITypeEnum {
return RedirectURITypeEnumFromJSONTyped(json, false);
}
export function RedirectURITypeEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): RedirectURITypeEnum {
return json as RedirectURITypeEnum;
}
export function RedirectURITypeEnumToJSON(value?: RedirectURITypeEnum | null): any {
return value as any;
}
export function RedirectURITypeEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): RedirectURITypeEnum {
return value as RedirectURITypeEnum;
}

View File

@@ -1,57 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const RedirectUriTypeEnum = {
Authorization: "authorization",
Logout: "logout",
UnknownDefaultOpenApi: "11184809",
} as const;
export type RedirectUriTypeEnum = (typeof RedirectUriTypeEnum)[keyof typeof RedirectUriTypeEnum];
export function instanceOfRedirectUriTypeEnum(value: any): boolean {
for (const key in RedirectUriTypeEnum) {
if (Object.prototype.hasOwnProperty.call(RedirectUriTypeEnum, key)) {
if (RedirectUriTypeEnum[key as keyof typeof RedirectUriTypeEnum] === value) {
return true;
}
}
}
return false;
}
export function RedirectUriTypeEnumFromJSON(json: any): RedirectUriTypeEnum {
return RedirectUriTypeEnumFromJSONTyped(json, false);
}
export function RedirectUriTypeEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): RedirectUriTypeEnum {
return json as RedirectUriTypeEnum;
}
export function RedirectUriTypeEnumToJSON(value?: RedirectUriTypeEnum | null): any {
return value as any;
}
export function RedirectUriTypeEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): RedirectUriTypeEnum {
return value as RedirectUriTypeEnum;
}

View File

@@ -72,12 +72,6 @@ export interface SourceType {
* @memberof SourceType
*/
readonly oidcJwksUrl: string | null;
/**
*
* @type {boolean}
* @memberof SourceType
*/
clientSecretRequired: boolean;
}
/**
@@ -93,8 +87,6 @@ export function instanceOfSourceType(value: object): value is SourceType {
if (!("profileUrl" in value) || value["profileUrl"] === undefined) return false;
if (!("oidcWellKnownUrl" in value) || value["oidcWellKnownUrl"] === undefined) return false;
if (!("oidcJwksUrl" in value) || value["oidcJwksUrl"] === undefined) return false;
if (!("clientSecretRequired" in value) || value["clientSecretRequired"] === undefined)
return false;
return true;
}
@@ -116,7 +108,6 @@ export function SourceTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean)
profileUrl: json["profile_url"],
oidcWellKnownUrl: json["oidc_well_known_url"],
oidcJwksUrl: json["oidc_jwks_url"],
clientSecretRequired: json["client_secret_required"],
};
}
@@ -144,6 +135,5 @@ export function SourceTypeToJSONTyped(
name: value["name"],
verbose_name: value["verboseName"],
urls_customizable: value["urlsCustomizable"],
client_secret_required: value["clientSecretRequired"],
};
}

View File

@@ -707,7 +707,7 @@ export * from "./RedirectStageModeEnum";
export * from "./RedirectStageRequest";
export * from "./RedirectURI";
export * from "./RedirectURIRequest";
export * from "./RedirectUriTypeEnum";
export * from "./RedirectURITypeEnum";
export * from "./RelatedGroup";
export * from "./RelatedRule";
export * from "./Reputation";

View File

@@ -9,7 +9,7 @@ dependencies = [
"argon2-cffi==25.1.0",
"cachetools==7.0.6",
"channels==4.3.2",
"cryptography==47.0.0",
"cryptography==48.0.0",
"dacite==1.9.2",
"deepmerge==2.0",
"defusedxml==0.7.1",
@@ -48,7 +48,7 @@ dependencies = [
"opencontainers==0.0.15",
"packaging==26.2",
"paramiko==4.0.0",
"psycopg[c,pool]==3.3.3",
"psycopg[c,pool]==3.3.4",
"pydantic-scim==0.0.8",
"pydantic==2.13.3",
"pyjwt==2.11.0",

View File

@@ -34608,10 +34608,12 @@ components:
properties:
brand:
type: string
nullable: true
family:
type: string
model:
type: string
nullable: true
required:
- brand
- family
@@ -34624,12 +34626,16 @@ components:
type: string
major:
type: string
nullable: true
minor:
type: string
nullable: true
patch:
type: string
nullable: true
patch_minor:
type: string
nullable: true
required:
- family
- major
@@ -44643,6 +44649,7 @@ components:
consumer_secret:
type: string
writeOnly: true
minLength: 1
additional_scopes:
type: string
oidc_well_known_url:
@@ -44659,6 +44666,7 @@ components:
token request flow
required:
- consumer_key
- consumer_secret
- name
- provider_type
- slug
@@ -49967,6 +49975,7 @@ components:
consumer_secret:
type: string
writeOnly: true
minLength: 1
additional_scopes:
type: string
oidc_well_known_url:
@@ -52672,7 +52681,6 @@ components:
ProviderTypeEnum:
enum:
- apple
- atproto
- openidconnect
- entraid
- azuread
@@ -53631,7 +53639,7 @@ components:
type: string
redirect_uri_type:
allOf:
- $ref: '#/components/schemas/RedirectUriTypeEnum'
- $ref: '#/components/schemas/RedirectURITypeEnum'
default: authorization
required:
- matching_mode
@@ -53647,12 +53655,12 @@ components:
minLength: 1
redirect_uri_type:
allOf:
- $ref: '#/components/schemas/RedirectUriTypeEnum'
- $ref: '#/components/schemas/RedirectURITypeEnum'
default: authorization
required:
- matching_mode
- url
RedirectUriTypeEnum:
RedirectURITypeEnum:
enum:
- authorization
- logout
@@ -56335,12 +56343,9 @@ components:
type: string
readOnly: true
nullable: true
client_secret_required:
type: boolean
required:
- access_token_url
- authorization_url
- client_secret_required
- name
- oidc_jwks_url
- oidc_well_known_url

View File

@@ -8,6 +8,8 @@ use eyre::{Result, eyre};
use tracing::{error, info, trace};
mod metrics;
#[cfg(feature = "proxy")]
mod outpost;
#[cfg(feature = "core")]
mod server;
#[cfg(feature = "core")]
@@ -29,6 +31,8 @@ enum Command {
Server(server::Cli),
#[cfg(feature = "core")]
Worker(worker::Cli),
#[cfg(feature = "proxy")]
Proxy(outpost::proxy::Cli),
}
#[derive(Debug, FromArgs, PartialEq)]
@@ -53,6 +57,8 @@ fn main() -> Result<()> {
Command::Server(_) => Mode::set(Mode::Server)?,
#[cfg(feature = "core")]
Command::Worker(_) => Mode::set(Mode::Worker)?,
#[cfg(feature = "proxy")]
Command::Proxy(_) => Mode::set(Mode::Proxy)?,
}
trace!("installing error formatting");
@@ -108,6 +114,10 @@ fn main() -> Result<()> {
let workers = worker::start(args, &mut tasks)?;
metrics.workers.store(Some(workers));
}
#[cfg(feature = "proxy")]
Command::Proxy(args) => {
outpost::start::<outpost::proxy::ProxyOutpost>(args, &mut tasks).await?;
}
}
let errors = tasks.run().await;

312
src/outpost/event.rs Normal file
View File

@@ -0,0 +1,312 @@
use std::{fmt::Display, sync::Arc};
use ak_common::{Arbiter, Tasks, VERSION, api, arbiter, authentik_build_hash};
use axum::http::{HeaderValue, header::AUTHORIZATION};
use eyre::{Result, eyre};
use futures::{SinkExt as _, StreamExt as _};
use nix::unistd::gethostname;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use time::UtcDateTime;
use tokio::{
signal::unix::SignalKind,
time::{Duration, interval, sleep},
};
use tokio_tungstenite::tungstenite::{Message, client::IntoClientRequest as _};
use tracing::{debug, info, instrument, trace, warn};
use url::Url;
use crate::outpost::{Outpost, OutpostController};
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone, Copy, Eq)]
#[repr(u8)]
enum EventKind {
/// Code used to acknowledge a previous message.
Ack = 0,
/// Code used to send a healthcheck keepalive.
Hello = 1,
/// Code received to trigger a config update.
TriggerUpdate = 2,
/// Code received to trigger some provider specific function.
ProviderSpecific = 3,
/// Code received to identify the end of a session.
SessionEnd = 4,
}
impl Display for EventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ack => write!(f, "Ack"),
Self::Hello => write!(f, "Hello"),
Self::TriggerUpdate => write!(f, "TriggerUpdate"),
Self::ProviderSpecific => write!(f, "ProviderSpecific"),
Self::SessionEnd => write!(f, "SessionEnd"),
}
}
}
#[derive(Serialize, Deserialize)]
struct Event {
instruction: EventKind,
args: serde_json::Value,
}
#[derive(Debug, Deserialize)]
pub(crate) struct EventSessionEnd {
session_id: String,
}
fn build_ws_url(mut url: Url, outpost_pk: &str, instance_uuid: &str, attempt: u32) -> Result<Url> {
let ws_scheme = match url.scheme() {
"https" => "wss",
"http" => "ws",
other => return Err(eyre!("Unsupported scheme for WebSocket URL: {other}")),
};
url.set_scheme(ws_scheme)
.map_err(|()| eyre!("Failed to set URL scheme to {ws_scheme}"))?;
url.set_path(&format!("{}ws/outpost/{outpost_pk}/", url.path()));
url.query_pairs_mut()
.append_pair("instance_uuid", instance_uuid)
.append_pair("attempt", &attempt.to_string());
Ok(url)
}
fn hello_args(instance_uuid: &str) -> serde_json::Value {
let raw_hostname = gethostname().unwrap_or_default();
let hostname = raw_hostname.to_string_lossy();
serde_json::json!({
"version": VERSION,
"buildHash": authentik_build_hash(None),
"uuid": instance_uuid,
// TODO: rust version and AWS-LC versions
"hostname": hostname,
})
}
#[instrument(skip_all)]
async fn handle_event<O: Outpost>(
controller: Arc<OutpostController>,
outpost: Arc<O>,
event: Event,
) -> Result<()> {
match event.instruction {
EventKind::Ack | EventKind::Hello => {}
EventKind::TriggerUpdate => {
info!("received update trigger, refreshing outpost");
sleep(controller.reload_offset).await;
controller.refresh().await?;
debug!("outpost controller has been refreshed");
outpost.refresh().await?;
debug!("outpost has been refreshed");
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "This is fine, we'll never get big values here."
)]
controller
.m_last_update
.set(UtcDateTime::now().unix_timestamp() as f64);
}
EventKind::SessionEnd => {
let event: EventSessionEnd = serde_json::from_value(event.args)?;
outpost.end_session(event).await?;
}
#[expect(
clippy::unimplemented,
reason = "this is only relevant for the RAC provider"
)]
EventKind::ProviderSpecific => unimplemented!(),
}
Ok(())
}
async fn watch_events_inner<O: Outpost>(
arbiter: Arbiter,
controller: Arc<OutpostController>,
outpost: Arc<O>,
attempt: u32,
) -> Result<()> {
let server_config = api::ServerConfig::new()?;
let ws_url = build_ws_url(
server_config.host,
&controller.outpost.load().pk.to_string(),
&controller.instance_uuid.to_string(),
attempt,
)?;
debug!(url = %ws_url, "connecting to websocket");
let mut request = ws_url.into_client_request()?;
let token = controller
.api_config
.bearer_access_token
.as_deref()
.unwrap_or("");
request.headers_mut().insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}"))?,
);
let (ws_stream, _response) = tokio_tungstenite::connect_async(request).await?;
let (mut ws_write, mut ws_read) = ws_stream.split();
info!(
outpost = %controller.outpost.load().pk,
"connected to websocket"
);
controller.m_connection.set(1_u8);
let get_refresh_interval = || {
let mut interval = controller.outpost.load().refresh_interval_s;
// Ensure timer interval is not negative or 0.
// If it is, we default to 5 minutes.
if interval <= 0_i32 {
interval = 60_i32 * 5_i32;
}
// Clamp interval to be at least 30 seconds.
if interval < 30_i32 {
interval = 30_i32;
}
// infallible because we bound it to be positive above
Duration::from_secs(interval.try_into().expect("infallible"))
};
let mut refresh_interval = interval(get_refresh_interval());
let mut heartbeat_interval = interval(Duration::from_secs(10));
let mut events_rx = arbiter.events_subscribe();
loop {
tokio::select! {
_ = refresh_interval.tick() => {
info!("refreshing outpost on interval");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
Event {
instruction: EventKind::TriggerUpdate,
args: serde_json::Value::Null
}
).await {
warn!(?err, "failed to refresh");
}
refresh_interval = interval(get_refresh_interval());
// Since we re-create the interval, we need to make it tick instantly to avoid
// ending up in a never-ending tick-loop.
refresh_interval.tick().await;
},
_ = heartbeat_interval.tick() => {
let ping = Event {
instruction: EventKind::Hello,
args: hello_args(&controller.instance_uuid.to_string()),
};
ws_write.send(Message::text(serde_json::to_string(&ping)?)).await?;
trace!("sent websocket hello (heartbeat)");
},
Ok(arbiter::Event::Signal(signal)) = events_rx.recv() => {
if signal == SignalKind::user_defined1() {
info!("refreshing outpost on signal");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
Event {
instruction: EventKind::TriggerUpdate,
args: serde_json::Value::Null
}
).await {
warn!(?err, "failed to refresh");
}
}
},
msg = ws_read.next() => {
let Some(msg) = msg else {
break;
};
let msg = msg?;
match msg {
Message::Text(text) => {
let Ok(event): Result<Event, _> = serde_json::from_str(&text) else {
warn!(data = text.as_str(), "failed to parse event");
continue;
};
trace!(event = %event.instruction, "received websocket event");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
event,
).await {
warn!(?err, "failed to handle event");
}
},
Message::Ping(data) => {
ws_write.send(Message::Pong(data)).await?;
},
Message::Close(_) => {
break;
},
_ => {},
}
},
() = arbiter.shutdown() => break,
}
}
Ok(())
}
async fn watch_events<O: Outpost>(
arbiter: Arbiter,
controller: Arc<OutpostController>,
outpost: Arc<O>,
) -> Result<()> {
const MAX_BACKOFF: Duration = Duration::from_mins(5);
let mut backoff = Duration::from_secs(1);
let mut attempt: u32 = 0;
loop {
tokio::select! {
() = arbiter.shutdown() => break,
res = watch_events_inner(
arbiter.clone(),
Arc::clone(&controller),
Arc::clone(&outpost),
attempt
) => {
controller.m_connection.set(0_u8);
match res {
Ok(()) => debug!("websocket disconnected cleanly"),
Err(err) => warn!(?err, attempt, "websocket error"),
}
info!(attempt, delay = backoff.as_secs(), "reconnecting websocket in {}s...", backoff.as_secs());
tokio::select! {
() = arbiter.shutdown() => break,
() = sleep(backoff) => {}
}
backoff = (backoff * 2).min(MAX_BACKOFF);
attempt += 1;
}
}
}
info!("stopping event watcher");
Ok(())
}
pub(crate) fn start<O: Outpost + 'static>(
tasks: &mut Tasks,
controller: Arc<OutpostController>,
outpost: Arc<O>,
) -> Result<()> {
let arbiter = tasks.arbiter();
tasks
.build_task()
.name(&format!("{}::watch_events", module_path!()))
.spawn(watch_events(arbiter, controller, outpost))?;
Ok(())
}

123
src/outpost/mod.rs Normal file
View File

@@ -0,0 +1,123 @@
use std::{sync::Arc, time::Duration};
use ak_client::{
apis::{configuration::Configuration, outposts_api::outposts_instances_list},
models::Outpost as OutpostModel,
};
use ak_common::{Tasks, VERSION, api, authentik_build_hash};
use arc_swap::ArcSwap;
use eyre::{Result, eyre};
use tracing::{debug, info, instrument};
use uuid::Uuid;
pub(crate) mod event;
#[cfg(feature = "proxy")]
pub(crate) mod proxy;
pub(crate) trait Outpost: Send + Sync + Sized {
const OUTPOST_TYPE: &'static str;
type Cli: Send + Sync;
async fn new(controller: Arc<OutpostController>) -> Result<Self>;
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()>;
fn refresh(&self) -> impl Future<Output = Result<()>> + Send;
fn end_session(&self, event: event::EventSessionEnd)
-> impl Future<Output = Result<()>> + Send;
}
#[derive(Debug)]
pub(crate) struct OutpostController {
api_config: Configuration,
outpost: ArcSwap<OutpostModel>,
instance_uuid: Uuid,
reload_offset: Duration,
m_info: metrics::Gauge,
m_last_update: metrics::Gauge,
m_connection: metrics::Gauge,
}
impl OutpostController {
#[instrument(skip_all)]
async fn get_outpost(api_config: &Configuration) -> Result<OutpostModel> {
let outposts = outposts_instances_list(
api_config, None, None, None, None, None, None, None, None, None, None, None, None,
)
.await?;
let Some(outpost) = outposts.results.into_iter().next() else {
return Err(eyre!(
"No outposts found with given token, ensure the given token corresponds to an \
authentik Outpost"
));
};
debug!(name = outpost.name, "fetched outpost configuration");
Ok(outpost)
}
#[instrument(skip_all)]
async fn new<O: Outpost>() -> Result<Self> {
let api_config = api::make_config()?;
let outpost = Self::get_outpost(&api_config).await?;
let instance_uuid = Uuid::new_v4();
let m_labels = [
("outpost_name", outpost.name.clone()),
("outpost_type", O::OUTPOST_TYPE.to_owned()),
("uuid", instance_uuid.to_string()),
("version", VERSION.to_owned()),
("build", authentik_build_hash(None)),
];
metrics::describe_gauge!("authentik_outpost_info", "Outpost info");
let m_info = metrics::gauge!("authentik_outpost_info", &m_labels);
metrics::describe_gauge!("authentik_outpost_last_update", "Time of last update");
let m_last_update = metrics::gauge!("authentik_outpost_last_update", &m_labels);
metrics::describe_gauge!("authentik_outpost_connection", "Connection status");
let m_connection = metrics::gauge!("authentik_outpost_connection", &m_labels);
let reload_offset = Duration::from_secs(rand::random_range(0..10));
let controller = Self {
api_config,
outpost: ArcSwap::from_pointee(outpost),
instance_uuid,
reload_offset,
m_info,
m_last_update,
m_connection,
};
info!(embedded = controller.is_embedded(), "outpost mode");
debug!(?reload_offset, "HA Reload offset");
Ok(controller)
}
fn is_embedded(&self) -> bool {
self.outpost
.load()
.managed
.as_ref()
.and_then(|m| m.as_deref())
.is_some_and(|m| m == "goauthentik.io/outposts/embedded")
}
async fn refresh(&self) -> Result<()> {
let outpost = Self::get_outpost(&self.api_config).await?;
self.outpost.swap(Arc::new(outpost));
Ok(())
}
}
#[instrument(skip_all)]
pub(crate) async fn start<O: Outpost + 'static>(_cli: O::Cli, tasks: &mut Tasks) -> Result<()> {
let controller = Arc::new(OutpostController::new::<O>().await?);
let outpost = Arc::new(O::new(Arc::clone(&controller)).await?);
event::start(tasks, Arc::clone(&controller), Arc::clone(&outpost))?;
outpost.start(tasks)?;
controller.m_info.set(1_u8);
Ok(())
}

View File

@@ -0,0 +1,41 @@
use ak_client::models::ProxyOutpostConfig;
use axum::Router;
use eyre::{Result, eyre};
use tracing::instrument;
use url::Url;
use crate::outpost::proxy::ProxyOutpost;
const REDIRECT_PARAM: &str = "rd";
const CALLBACK_SIGNATURE: &str = "X-authentik-auth-callback";
const LOGOUT_SIGNATURE: &str = "X-authentik-logout";
#[derive(Debug)]
pub(super) struct Application {
pub(super) host: String,
pub(super) provider: ProxyOutpostConfig,
pub(super) router: Router,
}
impl Application {
#[instrument(skip_all)]
pub(super) fn new(_existing_apps: &ProxyOutpost, provider: ProxyOutpostConfig) -> Result<Self> {
let external_url = Url::parse(&provider.external_host)?;
if !external_url.has_authority() {
return Err(eyre!("no host in external host"));
}
let external_host = external_url.authority();
let _redirect_url = {
let mut redirect_url = external_url.join("outpost.goauthentik.io/callback")?;
redirect_url.set_query(Some(&format!("{CALLBACK_SIGNATURE}=true")));
redirect_url
};
Ok(Self {
host: external_host.to_owned(),
provider,
router: Router::new(),
})
}
}

View File

@@ -0,0 +1,76 @@
use std::sync::Arc;
use tower::util::ServiceExt as _;
use ak_axum::{error::Result, extract::host::Host};
use axum::{
extract::{Request, State},
http::{Method, StatusCode, header::CONTENT_TYPE},
response::{IntoResponse as _, Response},
};
use metrics::histogram;
use serde_json::json;
use tokio::time::Instant;
use tracing::{Instrument as _, field, info_span, instrument, trace, warn};
use crate::outpost::proxy::ProxyOutpost;
#[instrument(skip_all)]
pub(super) async fn handle_ping(
method: Method,
Host(host): Host,
State(outpost): State<Arc<ProxyOutpost>>,
) -> Response {
let start = Instant::now();
histogram!(
"authentik_outpost_proxy_request_duration_seconds",
"outpost_name" => outpost.controller.outpost.load().name.clone(),
"method" => method.to_string(),
"host" => host,
"type" => "ping",
)
.record(start.elapsed().as_secs_f64());
StatusCode::NO_CONTENT.into_response()
}
#[instrument(skip_all)]
pub(super) async fn default(
method: Method,
Host(host): Host,
State(outpost): State<Arc<ProxyOutpost>>,
request: Request,
) -> Result<Response> {
let span = info_span!("proxy outpost request", user = field::Empty);
let start = Instant::now();
let Some(app) = outpost.lookup_app(&host) else {
trace!(headers = ?request.headers(), "tracing headers for no hostname match");
warn!("no app for hostname");
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.header(CONTENT_TYPE, "application/json")
.body(
json!({
"message": "no app for hostname",
"host": host,
"detail": format!("check the outpost settings and make sure '{host}' is included."),
})
.to_string()
.into(),
)
.expect("infallible"));
};
trace!("passing to application");
let response = app.router.clone().oneshot(request).instrument(span).await?;
histogram!(
"authentik_outpost_proxy_request_duration_seconds",
"outpost_name" => outpost.controller.outpost.load().name.clone(),
"method" => method.to_string(),
"host" => host,
"type" => "app",
)
.record(start.elapsed().as_secs_f64());
Ok(response)
}

187
src/outpost/proxy/mod.rs Normal file
View File

@@ -0,0 +1,187 @@
use std::{collections::HashMap, sync::Arc};
use ak_axum::router::wrap_router;
use ak_client::{apis::outposts_api::outposts_proxy_list, models::ProxyMode};
use ak_common::{Tasks, api::fetch_all, config};
use arc_swap::ArcSwap;
use argh::FromArgs;
use axum::Router;
use eyre::Result;
use tracing::{debug, error, info, instrument, warn};
use crate::outpost::{Outpost, OutpostController, proxy::application::Application};
mod application;
mod handlers;
#[derive(Debug, Default, FromArgs, PartialEq, Eq)]
/// Run the authentik proxy outpost.
#[argh(subcommand, name = "proxy")]
#[expect(
clippy::empty_structs_with_brackets,
reason = "argh doesn't support unit structs"
)]
pub(crate) struct Cli {}
pub(crate) struct ProxyOutpost {
controller: Arc<OutpostController>,
apps: ArcSwap<HashMap<String, Arc<Application>>>,
}
impl Outpost for ProxyOutpost {
type Cli = Cli;
const OUTPOST_TYPE: &'static str = "proxy";
#[instrument(skip_all)]
async fn new(controller: Arc<OutpostController>) -> Result<Self> {
Ok(Self {
controller,
apps: ArcSwap::from_pointee(HashMap::with_capacity(0)),
})
}
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()> {
let router = build_router(self);
for addr in config::get().listen.http.iter().copied() {
ak_axum::server::start_plain(tasks, "proxy-outpost", router.clone(), addr, false)?;
}
Ok(())
}
#[instrument(skip_all)]
async fn refresh(&self) -> Result<()> {
debug!(
outpost_pk = %self.controller.outpost.load().pk,
"requesting providers for outpost"
);
let providers = fetch_all(
|page| {
outposts_proxy_list(
&self.controller.api_config,
None,
None,
Some(page),
Some(100_i32),
None,
)
},
|r| &r.pagination,
|r| r.results,
)
.await
.inspect_err(|err| error!(?err, "failed to fetch providers"))?;
debug!(count = providers.len(), "fetched providers");
if providers.is_empty() && !self.controller.is_embedded() {
warn!(
"no providers assigned to this outpost, check outpost configuration in authentik"
);
}
for (i, provider) in providers.iter().enumerate() {
debug!(
index = i,
name = provider.name,
external_host = provider.external_host,
assigned_to_app = provider.assigned_application_name,
"provider details"
);
}
let mut apps = HashMap::with_capacity(providers.len());
for provider in providers {
let name = provider.name.clone();
let Ok(application) = Application::new(self, provider)
.inspect_err(|err| warn!(?err, "failed to setup application, skipping provider"))
else {
continue;
};
info!(name, host = application.host, "loaded application");
apps.insert(application.host.clone(), Arc::new(application));
}
self.apps.store(Arc::new(apps));
Ok(())
}
async fn end_session(&self, _event: super::event::EventSessionEnd) -> Result<()> {
// todo!()
warn!(?_event, "removing session");
Ok(())
}
}
impl ProxyOutpost {
#[instrument(skip(self))]
fn lookup_app(&self, host: &str) -> Option<Arc<Application>> {
let apps = self.apps.load();
// If we only have a single app, host name switching doesn't matter.
if apps.len() == 1
&& let Some(app) = apps.values().next()
{
debug!(app = app.provider.name, "found a single app, using it");
return Some(Arc::clone(app));
}
if let Some(app) = apps.get(host) {
debug!(app = app.provider.name, "found app based direct host match");
return Some(Arc::clone(app));
}
// For forward_auth_domain, we don't have a direct app to domain relationship.
// Check through all apps, and check how much of their cookie domain matches the host.
// Return the application that has the longest match.
let mut longest_match = None;
let mut longest_len = 0_usize;
for app in apps.values() {
if app.provider.mode != Some(ProxyMode::ForwardDomain) {
continue;
}
if let Some(cookie_domain) = app.provider.cookie_domain.as_deref() {
// Check if the cookie domain has a leading period for a wildcard.
// This will decrease the weight of a wildcard domain, but a request to example.com
// with the cookie domain set to example.com will still be routed correctly.
let domain = cookie_domain.trim_start_matches('.');
if host.ends_with(domain) && domain.len() > longest_len {
longest_len = domain.len();
longest_match = Some(Arc::clone(app));
}
// For forward_auth_domain, we need to response on the external domain too.
if app.provider.external_host == host {
debug!(app = app.provider.name, "found app based on external_host");
return Some(Arc::clone(app));
}
}
}
if let Some(app) = &longest_match {
debug!(app = app.provider.name, "found app based on cookie domain");
}
longest_match
}
}
fn build_router(outpost: Arc<ProxyOutpost>) -> Router {
wrap_router(
Router::new()
.nest(
"/outpost.goauthentik.io/ping",
Router::new().fallback(handlers::handle_ping),
)
.fallback(handlers::default)
.with_state(outpost),
true,
)
}

108
uv.lock generated
View File

@@ -318,7 +318,7 @@ requires-dist = [
{ name = "argon2-cffi", specifier = "==25.1.0" },
{ name = "cachetools", specifier = "==7.0.6" },
{ name = "channels", specifier = "==4.3.2" },
{ name = "cryptography", specifier = "==47.0.0" },
{ name = "cryptography", specifier = "==48.0.0" },
{ name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" },
@@ -357,7 +357,7 @@ requires-dist = [
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
{ name = "packaging", specifier = "==26.2" },
{ name = "paramiko", specifier = "==4.0.0" },
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.3.3" },
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.3.4" },
{ name = "pydantic", specifier = "==2.13.3" },
{ name = "pydantic-scim", specifier = "==0.0.8" },
{ name = "pyjwt", specifier = "==2.11.0" },
@@ -917,55 +917,55 @@ wheels = [
[[package]]
name = "cryptography"
version = "47.0.0"
version = "48.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" },
{ url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" },
{ url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" },
{ url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" },
{ url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" },
{ url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" },
{ url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" },
{ url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" },
{ url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" },
{ url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" },
{ url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" },
{ url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" },
{ url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" },
{ url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" },
{ url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" },
{ url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" },
{ url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" },
{ url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" },
{ url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" },
{ url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" },
{ url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" },
{ url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" },
{ url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" },
{ url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" },
{ url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" },
{ url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" },
{ url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" },
{ url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" },
{ url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" },
{ url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" },
{ url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" },
{ url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" },
{ url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" },
{ url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" },
{ url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" },
{ url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" },
{ url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" },
{ url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" },
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
]
[[package]]
@@ -2733,14 +2733,14 @@ wheels = [
[[package]]
name = "psycopg"
version = "3.3.3"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
]
[package.optional-dependencies]
@@ -2753,9 +2753,9 @@ pool = [
[[package]]
name = "psycopg-c"
version = "3.3.3"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/a0/8feb0ca8c7c20a8b9ac4d46b335ddd57e48e593b714262f006880f34fee5/psycopg_c-3.3.3.tar.gz", hash = "sha256:86ef6f4424348247828e83fb0882c9f8acb33e64d0a5ce66c1b4a5107ee73edd", size = 631965, upload-time = "2026-02-18T16:52:18.084Z" }
sdist = { url = "https://files.pythonhosted.org/packages/21/7c/c08364f2eab2913e4068b3b955d963e7a3491986a85429990969525def30/psycopg_c-3.3.4.tar.gz", hash = "sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22", size = 647111, upload-time = "2026-05-01T23:31:58.032Z" }
[[package]]
name = "psycopg-pool"
@@ -2947,14 +2947,14 @@ wheels = [
[[package]]
name = "pyopenssl"
version = "26.1.0"
version = "26.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/a8/26d36401e3ab8eed9030ad33f381da7856fcfad5691780fccd1b019718fc/pyopenssl-26.1.0.tar.gz", hash = "sha256:737f0a2275c5bc54f3b02137687e1a765931fb3949b9a92a825e4d33b9eec08b", size = 186181, upload-time = "2026-04-24T20:23:48.115Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/41/52f3a3e812b816a91e89aa504199d8bf989a1f873192b10762be66cf2009/pyopenssl-26.1.0-py3-none-any.whl", hash = "sha256:115563879b2c8ccb207975705d3e491434d8c9d7c79667c902ecbf5f3bbd2ece", size = 58109, upload-time = "2026-04-24T20:23:46.273Z" },
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
]
[[package]]

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
<path fill="#1185fe" d="M320 291.1C293.9 240.4 222.9 145.9 156.9 99.3C93.6 54.6 69.5 62.3 53.6 69.5C35.3 77.8 32 105.9 32 122.4C32 138.9 41.1 258 47 277.9C66.5 343.6 136.1 365.8 200.2 358.6C106.3 372.6 22.9 406.8 132.3 528.5C252.6 653.1 297.1 501.8 320 425.1C342.9 501.8 369.2 647.6 505.6 528.5C608 425.1 533.7 372.5 439.8 358.6C503.9 365.7 573.4 343.5 593 277.9C598.9 258 608 139 608 122.4C608 105.8 604.7 77.7 586.4 69.5C570.6 62.4 546.4 54.6 483.2 99.3C417.1 145.9 346.1 240.4 320 291.1Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 566 B

298
web/package-lock.json generated
View File

@@ -44,10 +44,10 @@
"@patternfly/patternfly": "^4.224.2",
"@playwright/test": "^1.59.1",
"@sentry/browser": "^10.50.0",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-links": "^10.3.5",
"@storybook/web-components": "^10.3.5",
"@storybook/web-components-vite": "^10.3.5",
"@storybook/addon-docs": "^10.3.6",
"@storybook/addon-links": "^10.3.6",
"@storybook/web-components": "^10.3.6",
"@storybook/web-components-vite": "^10.3.6",
"@types/codemirror": "^5.60.17",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.5",
@@ -114,7 +114,7 @@
"typescript": "^6.0.3",
"typescript-eslint": "^8.57.2",
"unist-util-visit": "^5.1.0",
"vite": "^8.0.8",
"vite": "^8.0.10",
"vitest": "^4.1.1",
"webcomponent-qr-code": "^1.3.0",
"wireit": "^0.14.12",
@@ -2895,9 +2895,9 @@
"license": "MIT"
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
"cpu": [
"arm64"
],
@@ -2911,9 +2911,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
"cpu": [
"arm64"
],
@@ -2927,9 +2927,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
"cpu": [
"x64"
],
@@ -2943,9 +2943,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
"cpu": [
"x64"
],
@@ -2959,9 +2959,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
"cpu": [
"arm"
],
@@ -2975,9 +2975,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
"cpu": [
"arm64"
],
@@ -2991,9 +2991,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
"cpu": [
"arm64"
],
@@ -3007,9 +3007,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
"cpu": [
"ppc64"
],
@@ -3023,9 +3023,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
"cpu": [
"s390x"
],
@@ -3039,9 +3039,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
"cpu": [
"x64"
],
@@ -3055,9 +3055,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
"cpu": [
"x64"
],
@@ -3071,9 +3071,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
"cpu": [
"arm64"
],
@@ -3087,27 +3087,48 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.9.2",
"@emnapi/runtime": "1.9.2",
"@napi-rs/wasm-runtime": "^1.1.3"
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
},
"engines": {
"node": ">=14.0.0"
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
"cpu": [
"arm64"
],
@@ -3121,9 +3142,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
"cpu": [
"x64"
],
@@ -3137,9 +3158,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs": {
@@ -3698,15 +3719,15 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.3.5.tgz",
"integrity": "sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==",
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.3.6.tgz",
"integrity": "sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==",
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.3.5",
"@storybook/csf-plugin": "10.3.6",
"@storybook/icons": "^2.0.1",
"@storybook/react-dom-shim": "10.3.5",
"@storybook/react-dom-shim": "10.3.6",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -3716,13 +3737,13 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.3.5"
"storybook": "^10.3.6"
}
},
"node_modules/@storybook/addon-links": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.3.5.tgz",
"integrity": "sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw==",
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.3.6.tgz",
"integrity": "sha512-tv9Xd68qRGBAvEubaxNo3FuFq4GwuMiBriD+gLGuFK0+/u3cnkuA264aoR1v6YCH3sT3er3+MBimuyKM3jLDxg==",
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0"
@@ -3733,7 +3754,7 @@
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.3.5"
"storybook": "^10.3.6"
},
"peerDependenciesMeta": {
"react": {
@@ -3742,12 +3763,12 @@
}
},
"node_modules/@storybook/builder-vite": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.5.tgz",
"integrity": "sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==",
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.6.tgz",
"integrity": "sha512-gpvR/sE4BcrFtmQZ+Ker7zD23oQzoVeqD9nF6cK6yzY+Q0svJXyX2EPmFG4y+EwygD5/vNzDpP84gGMut8VRwg==",
"license": "MIT",
"dependencies": {
"@storybook/csf-plugin": "10.3.5",
"@storybook/csf-plugin": "10.3.6",
"ts-dedent": "^2.0.0"
},
"funding": {
@@ -3755,14 +3776,14 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.3.5",
"storybook": "^10.3.6",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/@storybook/csf-plugin": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.3.5.tgz",
"integrity": "sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==",
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.3.6.tgz",
"integrity": "sha512-9kBf7VRdRqTSIYo+rPtVn5yjYYyK8kP2QhEYx3oiXvfwy4RexmbJnhk/tXa/lNiTqukA1TqaWQ2+5MqF4fu6YQ==",
"license": "MIT",
"dependencies": {
"unplugin": "^2.3.5"
@@ -3774,7 +3795,7 @@
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "^10.3.5",
"storybook": "^10.3.6",
"vite": "*",
"webpack": "*"
},
@@ -3810,9 +3831,9 @@
}
},
"node_modules/@storybook/react-dom-shim": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.3.5.tgz",
"integrity": "sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==",
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.3.6.tgz",
"integrity": "sha512-/Tu1gPu+Fw+zOnAGmxRmOD30FX3a04LxcTAKflEtdpmtIMVR5bA3qpjy+f5YhoyDCecbXyKmL1OeIU2FIIZHqQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -3821,13 +3842,13 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "^10.3.5"
"storybook": "^10.3.6"
}
},
"node_modules/@storybook/web-components": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.3.5.tgz",
"integrity": "sha512-tSppZagFCeZ+bWsaHUvdiw17ATWgfGDBz0mFicgEj0/eNuxQH2OvXyRIQUXY39b/55TBwSGeoIX3tOW91WIqpw==",
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.3.6.tgz",
"integrity": "sha512-femDZGYBGQDckL7F6ZCl2S+dNNBjvd9lp6rQrwBdbNprjctLd6d3EB4HyNM502QxtdEo7laq8y1goDu8KwIV3A==",
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -3840,24 +3861,24 @@
},
"peerDependencies": {
"lit": "^2.0.0 || ^3.0.0",
"storybook": "^10.3.5"
"storybook": "^10.3.6"
}
},
"node_modules/@storybook/web-components-vite": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.3.5.tgz",
"integrity": "sha512-6uAw6KAUXFsAPzp8KchcMp3gatEnEAd8ylIvzoMzvsIMiHmzXwvDNmoFZnAJ2tmsQGvF4dZRDCBg7PvWdTx8Rg==",
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.3.6.tgz",
"integrity": "sha512-VeDEAJuOOQV6VAqEF0pilXucS6kp+1ILJVkI+ets6Ku2D+RKeu167YrQAzh1NwzRTv0e5H0anDDNke+sWvg2dg==",
"license": "MIT",
"dependencies": {
"@storybook/builder-vite": "10.3.5",
"@storybook/web-components": "10.3.5"
"@storybook/builder-vite": "10.3.6",
"@storybook/web-components": "10.3.6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^10.3.5"
"storybook": "^10.3.6"
}
},
"node_modules/@swagger-api/apidom-ast": {
@@ -6527,12 +6548,12 @@
}
},
"node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
@@ -15811,13 +15832,13 @@
"license": "Unlicense"
},
"node_modules/rolldown": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.124.0",
"@rolldown/pluginutils": "1.0.0-rc.15"
"@oxc-project/types": "=0.127.0",
"@rolldown/pluginutils": "1.0.0-rc.17"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -15826,30 +15847,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
}
},
"node_modules/rolldown/node_modules/@oxc-project/types": {
"version": "0.124.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
}
},
"node_modules/rollup": {
@@ -16598,9 +16610,9 @@
}
},
"node_modules/storybook": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.5.tgz",
"integrity": "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==",
"version": "10.3.6",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.6.tgz",
"integrity": "sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==",
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
@@ -16625,11 +16637,15 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"prettier": "^2 || ^3"
"prettier": "^2 || ^3",
"vite-plus": "^0.1.15"
},
"peerDependenciesMeta": {
"prettier": {
"optional": true
},
"vite-plus": {
"optional": true
}
}
},
@@ -18400,16 +18416,16 @@
}
},
"node_modules/vite": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"version": "8.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.15",
"tinyglobby": "^0.2.15"
"postcss": "^8.5.10",
"rolldown": "1.0.0-rc.17",
"tinyglobby": "^0.2.16"
},
"bin": {
"vite": "bin/vite.js"
@@ -18490,6 +18506,22 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/vite/node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/vitest": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz",

View File

@@ -120,10 +120,10 @@
"@patternfly/patternfly": "^4.224.2",
"@playwright/test": "^1.59.1",
"@sentry/browser": "^10.50.0",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-links": "^10.3.5",
"@storybook/web-components": "^10.3.5",
"@storybook/web-components-vite": "^10.3.5",
"@storybook/addon-docs": "^10.3.6",
"@storybook/addon-links": "^10.3.6",
"@storybook/web-components": "^10.3.6",
"@storybook/web-components-vite": "^10.3.6",
"@types/codemirror": "^5.60.17",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.5",
@@ -190,7 +190,7 @@
"typescript": "^6.0.3",
"typescript-eslint": "^8.57.2",
"unist-util-visit": "^5.1.0",
"vite": "^8.0.8",
"vite": "^8.0.10",
"vitest": "^4.1.1",
"webcomponent-qr-code": "^1.3.0",
"wireit": "^0.14.12",

View File

@@ -52,10 +52,6 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
...AKModal.styles,
PFAbout,
css`
:host {
height: 100%;
}
.pf-c-about-modal-box {
--pf-c-about-modal-box--BackgroundColor: var(--ak-c-dialog--BackgroundColor);
width: unset;

View File

@@ -135,7 +135,7 @@ export class AdminInterface extends WithCapabilitiesConfig(
WebsocketClient.connect();
this.#sidebarMatcher = window.matchMedia("(width >= 1200px)");
this.#sidebarMatcher = window.matchMedia("(width > 1210px)");
this.sidebarOpen = this.#sidebarMatcher.matches;
}

View File

@@ -197,15 +197,17 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
?checked=${this.instance?.openInNewTab ?? false}
label=${msg("Open in new tab")}
help=${msg(
"Whether the launch URL will open in a new browser tab or window from the user's application library.",
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
)}
>
</ak-switch-input>
<ak-switch-input
name="metaHide"
?checked=${this.instance?.metaHide ?? false}
label=${msg("Hide from User Dashboard")}
help=${msg("Whether this application will be shown on the User Dashboard.")}
label=${msg("Hide from My applications")}
help=${msg(
"If checked, this application will not be shown on the user's My applications page.",
)}
>
</ak-switch-input>
<ak-file-search-input

View File

@@ -18,7 +18,7 @@ import {
RACProvider,
RadiusProvider,
RedirectURI,
RedirectUriTypeEnum,
RedirectURITypeEnum,
SAMLProvider,
SCIMProvider,
WSFederationProvider,
@@ -87,7 +87,7 @@ function formatRedirectUris(uris: RedirectURI[] = []) {
(${uri.matchingMode === MatchingModeEnum.Strict
? msg("strict")
: msg("regexp")},
${uri.redirectUriType === RedirectUriTypeEnum.Logout
${uri.redirectUriType === RedirectURITypeEnum.Logout
? msg("post logout")
: msg("authorization")})
</li>`,

View File

@@ -183,16 +183,16 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
?checked=${app.openInNewTab ?? false}
label=${msg("Open in new tab")}
help=${msg(
"Whether the launch URL will open in a new browser tab or window from the user's application library.",
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
)}
>
</ak-switch-input>
<ak-switch-input
name="metaHide"
?checked=${app.metaHide ?? false}
label=${msg("Hide from User Dashboard")}
label=${msg("Hide from My applications")}
help=${msg(
"Whether this application will be shown on the User Dashboard.",
"If checked, this application will not be shown on the user's My applications page.",
)}
>
</ak-switch-input>

View File

@@ -381,7 +381,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
return html`<h2 class="pf-c-wizard__main-title">
${msg("Review the Application and Provider")}
</h2>
<fieldset>
<fieldset class="ak-c-fieldset" name="application-details">
<legend>${msg("Application Details")}</legend>
<dl class="pf-c-description-list">
<div class="pf-c-description-list__group">
@@ -419,7 +419,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
${
renderer
? html`<fieldset>
? html`<fieldset class="ak-c-fieldset" name="provider-details">
<legend>${msg("Provider Details")}</legend>
${renderer(provider)}
</fieldset>`

View File

@@ -86,7 +86,7 @@ export class ConfigModal extends ModalButton {
></ak-codemirror>
</ak-expand>
</div>
<fieldset class="pf-c-modal-box__footer">
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
class="pf-c-button pf-m-plain"

View File

@@ -65,7 +65,7 @@ export class DeviceAddHowTo extends ModalButton {
})}
</ak-tabs>`}
</div>
<fieldset class="pf-c-modal-box__footer">
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
class="pf-c-button pf-m-primary"

View File

@@ -36,7 +36,7 @@ import {
OAuth2Provider,
OAuth2ProviderLogoutMethodEnum,
RedirectURI,
RedirectUriTypeEnum,
RedirectURITypeEnum,
SubModeEnum,
ValidationError,
} from "@goauthentik/api";
@@ -270,7 +270,7 @@ export function renderForm({
.newItem=${() => ({
matchingMode: MatchingModeEnum.Strict,
url: "",
redirectUriType: RedirectUriTypeEnum.Authorization,
redirectUriType: RedirectURITypeEnum.Authorization,
})}
.row=${(redirectURI: RedirectURI, idx: number) => {
return html`<ak-provider-oauth2-redirect-uri

View File

@@ -4,7 +4,7 @@ import { AKControlElement } from "#elements/ControlElement";
import { LitPropertyRecord } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { MatchingModeEnum, RedirectURI, RedirectUriTypeEnum } from "@goauthentik/api";
import { MatchingModeEnum, RedirectURI, RedirectURITypeEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, html } from "lit";
@@ -37,7 +37,7 @@ export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
public redirectURI: RedirectURI = {
matchingMode: MatchingModeEnum.Strict,
url: "",
redirectUriType: RedirectUriTypeEnum.Authorization,
redirectUriType: RedirectURITypeEnum.Authorization,
};
@property({ type: String, useDefault: true })
@@ -89,15 +89,15 @@ export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
@change=${onChange}
>
<option
value="${RedirectUriTypeEnum.Authorization}"
value="${RedirectURITypeEnum.Authorization}"
?selected=${(this.redirectURI.redirectUriType ??
RedirectUriTypeEnum.Authorization) === RedirectUriTypeEnum.Authorization}
RedirectURITypeEnum.Authorization) === RedirectURITypeEnum.Authorization}
>
${msg("Authorization")}
</option>
<option
value="${RedirectUriTypeEnum.Logout}"
?selected=${this.redirectURI.redirectUriType === RedirectUriTypeEnum.Logout}
value="${RedirectURITypeEnum.Logout}"
?selected=${this.redirectURI.redirectUriType === RedirectURITypeEnum.Logout}
>
${msg("Post Logout")}
</option>

View File

@@ -30,7 +30,6 @@ import {
GroupMatchingModeEnum,
OAuthSource,
OAuthSourceRequest,
PatchedOAuthSourceRequest,
PKCEMethodEnum,
ProviderTypeEnum,
SourcesApi,
@@ -82,20 +81,6 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
//#region Lifecycle
private get isAtProtocolSource(): boolean {
return (
this.providerType?.name === ProviderTypeEnum.Atproto ||
this.modelName?.includes("atproto") === true
);
}
private get isClientSecretRequired(): boolean {
if (this.isAtProtocolSource) {
return false;
}
return this.providerType?.clientSecretRequired !== false;
}
protected async loadInstance(pk: string): Promise<OAuthSource> {
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthRetrieve({
slug: pk,
@@ -112,20 +97,16 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
protected async send(data: OAuthSource): Promise<OAuthSource> {
data.providerType = (this.providerType?.name || "") as ProviderTypeEnum;
const requestData = data as unknown as OAuthSourceRequest & PatchedOAuthSourceRequest;
if (!this.isClientSecretRequired) {
requestData.consumerSecret = "";
}
if (this.instance) {
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthPartialUpdate({
slug: this.instance.slug,
patchedOAuthSourceRequest: requestData,
patchedOAuthSourceRequest: data,
});
}
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
oAuthSourceRequest: requestData,
oAuthSourceRequest: data as unknown as OAuthSourceRequest,
});
}
@@ -205,11 +186,9 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
autocomplete="off"
/>
<p class="pf-c-form__helper-text">
${this.isAtProtocolSource
? msg("URL used to create pushed authorization requests.")
: msg(
"URL used to request the initial token. This URL is only required for OAuth 1.",
)}
${msg(
"URL used to request the initial token. This URL is only required for OAuth 1.",
)}
</p>
</ak-form-element-horizontal> `
: nothing}
@@ -426,22 +405,16 @@ export class OAuthSourceForm extends BaseSourceForm<OAuthSource> {
spellcheck="false"
required
/>
<p class="pf-c-form__helper-text">
${this.isAtProtocolSource
? msg("Client metadata URL.")
: msg("Also known as Client ID.")}
</p>
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
</ak-form-element-horizontal>
${this.isClientSecretRequired
? html`<ak-secret-textarea-input
label=${msg("Consumer secret")}
name="consumerSecret"
input-hint="code"
help=${msg("Also known as Client Secret.")}
?required=${!this.instance}
?revealed=${!this.instance}
></ak-secret-textarea-input>`
: nothing}
<ak-secret-textarea-input
label=${msg("Consumer secret")}
name="consumerSecret"
input-hint="code"
help=${msg("Also known as Client Secret.")}
?required=${!this.instance}
?revealed=${!this.instance}
></ak-secret-textarea-input>
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
<input
type="text"

View File

@@ -37,8 +37,6 @@ export function ProviderToLabel(provider?: ProviderTypeEnum): string {
return "";
case ProviderTypeEnum.Apple:
return "Apple";
case ProviderTypeEnum.Atproto:
return "AT Protocol";
case ProviderTypeEnum.Azuread:
return "Azure Active Directory (Deprecated)";
case ProviderTypeEnum.Discord:

View File

@@ -50,7 +50,9 @@ export class RedirectStageForm extends BaseStageForm<RedirectStage> {
protected override renderForm(): TemplateResult {
return html`<span>
${msg("Redirect the user to another flow, potentially with all gathered context")}
${msg(
"Redirect the user to a static URL or another flow, optionally with all gathered context.",
)}
</span>
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input

View File

@@ -159,7 +159,7 @@ export class UserBulkRevokeSessionsForm extends ModalButton {
>
</ak-user-bulk-revoke-sessions-table>
</section>
<fieldset class="pf-c-modal-box__footer">
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<ak-spinner-button
.callAction=${async () => {

View File

@@ -48,7 +48,7 @@
width: 100%;
}
@media (width < 1200px) {
@media (width <= 1210px) {
column-gap: calc(var(--pf-global--spacer--md) / 2);
}
}
@@ -137,7 +137,7 @@
display: none;
}
@media (width < 1200px) {
@media (width <= 1210px) {
display: none;
}
}
@@ -164,7 +164,7 @@
grid-area: toggle;
}
@media (width >= 1200px) {
@media (width > 1210px) {
slot[name="toggle"] {
display: none;
}

View File

@@ -22,11 +22,12 @@ function resolvePath(...args: string[]): string {
* - Intercepts local links and scrolls to the target element.
*/
export const MDXAnchor = ({
href,
href: initialHref,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const { publicDirectory } = useMDXModule();
let href = initialHref;
if (href?.startsWith(".") && publicDirectory) {
const nextPathname = resolvePath(publicDirectory, href);

View File

@@ -12,7 +12,7 @@
);
--ak-c-command-palette__group--Color: var(--pf-global--palette--purple-100);
--ak-fieldset--BorderColor: transparent;
--ak-c-fieldset--BorderColor: transparent;
--ak-c-command-palette__item--BackgroundColor: transparent;
--ak-c-command-palette__item--Color: var(--pf-global--palette--purple-50);
@@ -37,7 +37,7 @@
transform: translate3d(0, 0, 0); /* Fixes rendering artifacts. */
@media (prefers-contrast: more) {
--ak-fieldset--BorderColor: var(--pf-global--palette--purple-500);
--ak-c-fieldset--BorderColor: var(--pf-global--palette--purple-500);
}
}
@@ -109,7 +109,7 @@
transition-duration: 0.2s;
legend {
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
cursor: pointer;
color: var(--ak-c-command-palette__group--Color);

View File

@@ -524,7 +524,7 @@ export class AKCommandPaletteModal extends AKModal {
Object.entries(grouped),
(_group, groupIdx) => `group-${groupIdx}`,
([groupLabel, commands], groupIdx) => html`
<fieldset part="results-group">
<fieldset class="ak-c-fieldset" part="results-group">
<legend
class="${!groupLabel ? "sr-only more-contrast-only" : ""}"
data-label=${ifPresent(groupLabel)}

View File

@@ -115,8 +115,8 @@
/* #region Footer */
fieldset.ak-c-dialog__footer {
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
padding-block: calc(var(--ak-fieldset__legend--PaddingInlineBase) / 2);
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--md);
padding-block: calc(var(--ak-c-fieldset__legend--PaddingInlineBase) / 2);
border-inline: none;
border-block-end: none;

View File

@@ -77,7 +77,7 @@ export class ConfirmationForm extends ModalButton {
<slot class="pf-c-content" name="body"></slot>
</form>
</section>
<fieldset class="pf-c-modal-box__footer">
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<ak-spinner-button
.callAction=${async () => {

View File

@@ -119,7 +119,7 @@ export class DeleteBulkForm<T> extends ModalButton {
>
</ak-used-by-table>
</section>
<fieldset class="pf-c-modal-box__footer">
<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<ak-spinner-button
.callAction=${async () => {

View File

@@ -218,7 +218,7 @@ export class ModalForm extends ModalButton {
}
protected renderActions(): SlottedTemplateResult {
return html`<fieldset class="pf-c-modal-box__footer">
return html`<fieldset class="ak-c-fieldset pf-c-modal-box__footer">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
type="button"

View File

@@ -126,7 +126,7 @@ export class FlowInspector extends AKElement {
protected renderNextStage({ currentPlan, isCompleted }: FlowInspection): TemplateResult {
return html`<div class="pf-c-card">
<fieldset>
<fieldset class="ak-c-fieldset">
<legend class="pf-c-card__title">${msg("Next stage")}</legend>
<div class="pf-c-card__body">
<dl class="pf-c-description-list">
@@ -184,7 +184,7 @@ ${stringify(this.getStage(currentPlan?.nextPlannedStage?.stageObj))}</pre
currentPlan,
}: FlowInspection): TemplateResult {
return html`<div class="pf-c-card">
<fieldset>
<fieldset class="ak-c-fieldset">
<legend class="pf-c-card__title">${msg("Plan history")}</legend>
<div class="pf-c-card__body">
<ol class="pf-c-progress-stepper pf-m-vertical">
@@ -248,7 +248,7 @@ ${stringify(this.getStage(currentPlan?.nextPlannedStage?.stageObj))}</pre
protected renderCurrentPlan({ currentPlan }: FlowInspection): TemplateResult {
return html`<div class="pf-c-card">
<fieldset>
<fieldset class="ak-c-fieldset">
<legend class="pf-c-card__title">${msg("Current plan context")}</legend>
<pre class="pf-c-card__body"><code>${stringify(
currentPlan?.planContext,
@@ -259,7 +259,7 @@ ${stringify(this.getStage(currentPlan?.nextPlannedStage?.stageObj))}</pre
protected renderSession({ currentPlan }: FlowInspection): TemplateResult {
return html`<div class="pf-c-card">
<fieldset>
<fieldset class="ak-c-fieldset">
<legend class="pf-c-card__title">${msg("Session ID")}</legend>
<div class="pf-c-card__body">
<code class="break"> ${currentPlan?.sessionId} </code>

View File

@@ -118,7 +118,7 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes
<p>${msg("You're about to be redirected to the following URL.")}</p>
<code>${this.getURL()}</code>
</div>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<a
type="submit"

View File

@@ -38,7 +38,7 @@ export class AccessDeniedStage extends BaseStage<
: nothing}
</ak-empty-state>
${this.challenge?.flowInfo?.cancelUrl
? html`<fieldset class="pf-c-form__group pf-m-action">
? html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<a
class="pf-c-button pf-m-primary pf-m-block"

View File

@@ -87,7 +87,7 @@ export class AuthenticatorDuoStage extends BaseStage<
</p>
<a href=${this.challenge.activationCode}>${msg("Duo activation")}</a>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
type="button"

View File

@@ -64,7 +64,7 @@ export class AuthenticatorEmailStage extends BaseStage<
${AKFormErrors({ errors: this.challenge?.responseErrors?.email })}
</div>
${this.renderNonFieldErrors()}
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"
@@ -120,7 +120,7 @@ export class AuthenticatorEmailStage extends BaseStage<
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
</div>
${this.renderNonFieldErrors()}
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"

View File

@@ -67,7 +67,7 @@ export class AuthenticatorSMSStage extends BaseStage<
${AKFormErrors({ errors: this.challenge.responseErrors?.phone_number })}
</div>
${this.renderNonFieldErrors()}
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"
@@ -106,7 +106,7 @@ export class AuthenticatorSMSStage extends BaseStage<
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
</div>
${this.renderNonFieldErrors()}
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"

View File

@@ -66,7 +66,7 @@ export class AuthenticatorStaticStage extends BaseStage<
</ul>
<p>${msg("Make sure to keep these tokens in a safe place.")}</p>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"

View File

@@ -175,7 +175,7 @@ export class AuthenticatorTOTPStage extends BaseStage<
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
</div>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"

View File

@@ -1,3 +1,13 @@
.ak-c-fieldset.ak-c-fieldset.pf-c-form__group.pf-m-action[name="device-challenges"] {
--ak-c-fieldset--BorderWidth: thin;
--ak-c-fieldset__legend--MarginInlineBase: var(--pf-global--spacer--sm);
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--sm);
--ak-c-fieldset--RowGap: 0;
--ak-c-fieldset--ColumnGap: var(--pf-global--spacer--sm);
}
.authenticator-button,
ak-stage-authenticator-validate.style-scope .authenticator-button {
align-items: center;
@@ -5,6 +15,7 @@ ak-stage-authenticator-validate.style-scope .authenticator-button {
display: grid;
grid-template-columns: minmax(auto, 2rem) minmax(33%, max-content);
gap: var(--pf-global--spacer--lg);
padding-block: calc(var(--pf-global--spacer--form-element) * 2);
&:hover {
background-color: var(--pf-global--BackgroundColor--200);

View File

@@ -267,7 +267,10 @@ export class AuthenticatorValidateStage
},
);
return html`<fieldset class="pf-c-form__group pf-m-action" name="device-challenges">
return html`<fieldset
class="ak-c-fieldset pf-c-form__group pf-m-action"
name="device-challenges"
>
<legend class="pf-c-title">${msg("Select an authentication method")}</legend>
${deviceChallengeButtons}
</fieldset>`;
@@ -300,7 +303,7 @@ export class AuthenticatorValidateStage
},
);
return html`<fieldset class="pf-c-form__group pf-m-action" name="stages">
return html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action" name="stages">
<legend class="sr-only">${msg("Select a configuration stage")}</legend>
${stageButtons}
</fieldset>`;

View File

@@ -30,7 +30,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
return html`<form class="pf-c-form" @submit=${this.submitForm}>
${this.renderUserInfo()}
<fieldset class="pf-c-form__group">
<fieldset class="ak-c-fieldset pf-c-form__group">
<legend class="sr-only">${msg("Authentication code")}</legend>
${AKLabel(
{
@@ -62,7 +62,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
${AKFormErrors({ errors: this.challenge?.responseErrors?.code })}
</fieldset>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button name="continue" type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}

View File

@@ -63,7 +63,7 @@ export class AuthenticatorValidateStageWebDuo extends BaseDeviceStage<
>
</ak-empty-state>
${this.showBackButton
? html`<fieldset class="pf-c-form__group pf-m-action">
? html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
${this.renderReturnToDevicePicker()}
</fieldset>`

View File

@@ -133,7 +133,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
>
</ak-empty-state>
${!this.authenticating || this.showBackButton
? html`<fieldset class="pf-c-form__group pf-m-action">
? html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
${!this.authenticating
? html`<button

View File

@@ -153,7 +153,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
${this.challenge?.responseErrors
? html`<p>${this.challenge.responseErrors.response[0].string}</p>`
: nothing}
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
${!this.registerRunning
? html` <button

View File

@@ -126,7 +126,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
? this.renderAdditional()
: this.renderNoPrevious()}
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"

View File

@@ -23,7 +23,7 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
return html`<ak-flow-card .challenge=${this.challenge}>
<form class="pf-c-form" @submit=${this.submitForm}>
<p>${msg(str`Stage name: ${this.challenge?.name}`)}</p>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"

View File

@@ -25,7 +25,7 @@ export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponse
<p>${msg("Check your Inbox for a verification email.")}</p>
</div>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"

View File

@@ -296,8 +296,15 @@ export class IdentificationStage extends BaseStage<
type: string,
label: string,
initialUserIdentification: string | null,
autocomplete: string,
passwordFields?: boolean,
) {
// When webauthn is enabled, add "webauthn" to autocomplete to enable passkey autofill
let autocomplete: AutoFill = type === "email" ? "email" : "username";
if (this.#webauthn.live) {
autocomplete = `${autocomplete} webauthn`;
}
return html`<input
${ref(this.autofocusTarget.reference)}
id=${id}
@@ -307,6 +314,9 @@ export class IdentificationStage extends BaseStage<
autofocus
autocomplete=${autocomplete}
spellcheck="false"
inputmode=${type === "email" ? "email" : "text"}
autocapitalize="none"
enterkeyhint=${passwordFields ? "next" : "go"}
class="pf-c-form-control"
value=${initialUserIdentification ?? ""}
required
@@ -345,19 +355,11 @@ export class IdentificationStage extends BaseStage<
const type = fields.length === 1 && fields[0] === UserFieldsEnum.Email ? "email" : "text";
const label = OR_LIST_FORMATTERS.format(fields.map((f) => UI_FIELDS[f]));
// When webauthn is enabled, add "webauthn" to autocomplete to enable passkey autofill
const autocomplete: AutoFill = this.#webauthn.live ? "username webauthn" : "username";
console.debug(
"Rendering identification stage with fields:",
fields,
initialUserIdentification,
);
// prettier-ignore
return html`${offerRecovery ? this.renderRecoveryMessage() : nothing}
<div class="pf-c-form__group">
${AKLabel({ required: true, htmlFor: inputID }, label)}
${this.renderUidField(inputID, type, label, initialUserIdentification, autocomplete)}
${this.renderUidField(inputID, type, label, initialUserIdentification, passwordFields)}
${rememberMeController?.renderToggleInput() ?? null}
${AKFormErrors({ errors: challenge.responseErrors?.uid_field })}
</div>
@@ -433,9 +435,8 @@ export class IdentificationStage extends BaseStage<
return html`<fieldset
slot="footer"
part="source-list"
role="group"
name="login-sources"
class="pf-c-form__group"
class="ak-c-fieldset pf-c-form__group"
>
<legend class="sr-only">${msg("Login sources")}</legend>
${repeat(
@@ -467,7 +468,8 @@ export class IdentificationStage extends BaseStage<
return html`<fieldset
slot="footer-band"
part="additional-actions"
class="pf-c-login__main-footer-band"
name="additional-actions"
class="ak-c-fieldset pf-c-login__main-footer-band"
>
<legend class="sr-only">${msg("Additional actions")}</legend>
${enrollUrl

View File

@@ -52,7 +52,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
?allow-show-password=${!!this.challenge?.allowShowPassword}
prefill=${PasswordManagerPrefill.password ?? ""}
></ak-flow-input-password>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button
name="continue"
@@ -67,7 +67,8 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
? html`<fieldset
slot="footer-band"
part="additional-actions"
class="pf-c-login__main-footer-band"
name="additional-actions"
class="ak-c-fieldset pf-c-login__main-footer-band"
>
<legend class="sr-only">${msg("Additional actions")}</legend>
<div class="pf-c-login__main-footer-band-item">

View File

@@ -322,7 +322,7 @@ ${prompt.initialValue}</textarea
}
protected renderContinue(): SlottedTemplateResult {
return html`<fieldset class="pf-c-form__group pf-m-action">
return html`<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button name="continue" type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}

View File

@@ -50,7 +50,7 @@ export class PasswordStage extends BaseStage<
</p>
</div>
<fieldset class="pf-c-form__group pf-m-action">
<fieldset class="ak-c-fieldset pf-c-form__group pf-m-action">
<legend class="sr-only">${msg("Form actions")}</legend>
<button name="remember-me" type="submit" class="pf-c-button pf-m-primary">
${msg("Yes")}

View File

@@ -13,6 +13,7 @@
@import "./components/Content/content.css";
@import "./components/Table/table.css";
@import "./components/Form/form.css";
@import "./components/Fieldset/fieldset.css";
@import "./components/Switch/switch.css";
@import "./components/Select/select.css";
@import "./components/Modal/modal.css";

View File

@@ -245,3 +245,94 @@ html[data-theme="dark"],
);
}
}
.ak-c-fieldset {
--ak-c-fieldset--BorderWidth: thin;
--ak-c-fieldset__legend--MarginInlineBase: var(--pf-global--spacer--sm);
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--sm);
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BackgroundColor--light-100));
@media (prefers-contrast: more) {
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BorderColor--200));
}
@media (prefers-contrast: less) {
border-color: var(--ak-c-fieldset--BorderColor, transparent);
}
border-width: var(--ak-c-fieldset--BorderWidth);
padding: var(--ak-c-fieldset__legend--PaddingInlineBase) !important;
& > legend {
line-height: 1;
padding: var(--ak-c-fieldset__legend--PaddingInlineBase) !important;
margin-inline-start: var(
--ak-c-fieldset__legend--MarginInlineStart,
var(--ak-c-fieldset__legend--MarginInlineBase)
) !important;
margin-inline-end: var(
--ak-legend-margin-inline-end,
var(--ak-c-fieldset__legend--MarginInlineBase)
) !important;
}
&:has(legend.sr-only:not(.more-contrast-only)) {
border-width: 0;
&:not(.pf-c-modal-box__footer) {
--ak-c-fieldset__legend--PaddingInlineBase: 0;
--ak-c-fieldset__legend--MarginInlineBase: 0;
}
}
&.pf-c-form__group {
border-radius: var(--pf-global--BorderRadius--sm);
}
&.pf-c-form__group {
display: flex;
flex-wrap: wrap;
&.pf-m-action {
gap: var(--pf-global--spacer--md) var(--pf-global--spacer--sm);
margin-block-start: 0;
/* Fallback for action-only fieldsets when :has() does not suppress
* the browser's native groove border. Keep the device picker
* bordered because it has a visible legend.
*/
&:not([name="device-challenges"]) {
border-width: 0;
--ak-c-fieldset__legend--PaddingInlineBase: 0;
--ak-c-fieldset__legend--MarginInlineBase: 0;
}
}
}
&.pf-c-login__main-footer-band {
& > *:last-child {
padding-block-end: var(--pf-c-login__main-footer-band-item--PaddingTop);
}
}
&.pf-c-modal-box__footer {
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--lg);
--pf-c-modal-box__footer--c-button--MarginRight: 0;
gap: var(--pf-global--spacer--sm);
justify-content: end;
padding-block: calc(var(--ak-c-fieldset__legend--PaddingInlineBase) / 2);
border-inline: none;
border-block-end: none;
--pf-c-modal-box__footer--c-button--sm--MarginRight: var(
--pf-c-modal-box__footer--c-button--MarginRight
);
& > ak-spinner-button:not(:last-child) {
margin-right: var(--pf-c-modal-box__footer--c-button--MarginRight);
}
}
}

View File

@@ -1,13 +1,13 @@
.pf-c-card > fieldset {
.pf-c-card > .ak-c-fieldset {
margin-inline: var(--pf-global--spacer--md);
margin-block-end: var(--pf-global--spacer--md);
@media not (prefers-contrast: more) {
--ak-fieldset__legend--MarginInlineStart: calc(
var(--pf-c-card--child--PaddingLeft) - var(--ak-fieldset__legend--PaddingInlineBase)
--ak-c-fieldset__legend--MarginInlineStart: calc(
var(--pf-c-card--child--PaddingLeft) - var(--ak-c-fieldset__legend--PaddingInlineBase)
);
--ak-legend-margin-inline-end: calc(
var(--pf-c-card--child--PaddingRight) - var(--ak-fieldset__legend--PaddingInlineBase)
var(--pf-c-card--child--PaddingRight) - var(--ak-c-fieldset__legend--PaddingInlineBase)
);
border-width: 0;

View File

@@ -0,0 +1,102 @@
.ak-c-fieldset {
--ak-c-fieldset--BorderWidth: thin;
--ak-c-fieldset--RowGap: var(--pf-global--spacer--md);
--ak-c-fieldset--ColumnGap: var(--pf-global--spacer--sm);
--ak-c-fieldset__legend--MarginInlineBase: var(--pf-global--spacer--sm);
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--sm);
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BackgroundColor--light-100));
border-width: var(--ak-c-fieldset--BorderWidth);
padding: var(--ak-c-fieldset__legend--PaddingInlineBase) !important;
@media (prefers-contrast: more) {
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BorderColor--200));
}
@media (prefers-contrast: less) {
border-color: var(--ak-c-fieldset--BorderColor, transparent);
}
}
.ak-c-fieldset > legend {
line-height: 1;
padding: var(--ak-c-fieldset__legend--PaddingInlineBase) !important;
margin-inline-start: var(
--ak-c-fieldset__legend--MarginInlineStart,
var(--ak-c-fieldset__legend--MarginInlineBase)
) !important;
margin-inline-end: var(
--ak-legend-margin-inline-end,
var(--ak-c-fieldset__legend--MarginInlineBase)
) !important;
}
.ak-c-fieldset:has(legend.sr-only:not(.more-contrast-only)) {
border-width: 0;
&:not(.pf-c-modal-box__footer) {
--ak-c-fieldset__legend--PaddingInlineBase: 0;
--ak-c-fieldset__legend--MarginInlineBase: 0;
}
}
.ak-c-fieldset.pf-c-form__group {
border-radius: var(--pf-global--BorderRadius--sm);
}
.ak-c-fieldset.pf-c-form__group {
display: flex;
flex-wrap: wrap;
&.pf-m-action {
--ak-c-fieldset__legend--PaddingInlineBase: 0;
--ak-c-fieldset__legend--MarginInlineBase: 0;
--ak-c-fieldset--BorderWidth: 0;
row-gap: var(--ak-c-fieldset--RowGap);
column-gap: var(--ak-c-fieldset--ColumnGap);
margin-block-start: 0;
}
}
.ak-c-fieldset.pf-c-login__main-footer-band > *:last-child {
padding-block-end: var(--pf-c-login__main-footer-band-item--PaddingTop);
}
/* TODO: Remove after ak-modal migration. */
.ak-c-fieldset.pf-c-modal-box__footer {
--ak-c-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--lg);
--pf-c-modal-box__footer--c-button--MarginRight: 0;
--pf-c-modal-box__footer--c-button--sm--MarginRight: var(
--pf-c-modal-box__footer--c-button--MarginRight
);
gap: var(--pf-global--spacer--sm);
justify-content: end;
padding-block: calc(var(--ak-c-fieldset__legend--PaddingInlineBase) / 2);
border-inline: none;
border-block-end: none;
& > ak-spinner-button:not(:last-child) {
margin-right: var(--pf-c-modal-box__footer--c-button--MarginRight);
}
}
[data-theme="dark"] .ak-c-fieldset,
:host([theme="dark"]) .ak-c-fieldset {
border-color: var(
--ak-c-fieldset--BorderColor,
var(--pf-global--BackgroundColor--dark-transparent-200)
);
@media (prefers-contrast: more) {
border-color: var(--ak-c-fieldset--BorderColor, var(--pf-global--BorderColor--300));
}
@media (prefers-contrast: less) {
border-color: var(--ak-c-fieldset--BorderColor, transparent);
}
}

View File

@@ -20,6 +20,11 @@
);
}
.pf-c-form__alert {
display: grid;
gap: var(--pf-global--spacer--form-element);
}
.pf-c-form.ak-m-content-center {
--pf-c-form--GridGap: var(--pf-global--spacer--sm);
@@ -90,91 +95,6 @@ ak-form-element-horizontal:has(.pf-c-form__helper-text + ak-checkbox-group) {
}
}
/* #region Fields */
fieldset {
--ak-fieldset--BorderWidth: thin;
--ak-fieldset__legend--MarginInlineBase: var(--pf-global--spacer--sm);
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--sm);
border-color: var(--ak-fieldset--BorderColor, var(--pf-global--BackgroundColor--light-100));
@media (prefers-contrast: more) {
border-color: var(--ak-fieldset--BorderColor, var(--pf-global--BorderColor--200));
}
@media (prefers-contrast: less) {
border-color: var(--ak-fieldset--BorderColor, transparent);
}
border-width: var(--ak-fieldset--BorderWidth);
padding: var(--ak-fieldset__legend--PaddingInlineBase) !important;
& > legend {
line-height: 1;
padding: var(--ak-fieldset__legend--PaddingInlineBase) !important;
margin-inline-start: var(
--ak-fieldset__legend--MarginInlineStart,
var(--ak-fieldset__legend--MarginInlineBase)
) !important;
margin-inline-end: var(
--ak-legend-margin-inline-end,
var(--ak-fieldset__legend--MarginInlineBase)
) !important;
}
&:has(legend.sr-only:not(.more-contrast-only)) {
border-width: 0;
&:not(.pf-c-modal-box__footer) {
--ak-fieldset__legend--PaddingInlineBase: 0;
--ak-fieldset__legend--MarginInlineBase: 0;
}
}
&.pf-c-form__group {
border-radius: var(--pf-global--BorderRadius--sm);
}
&.pf-c-form__group {
display: flex;
flex-wrap: wrap;
&.pf-m-action {
gap: var(--pf-global--spacer--md) var(--pf-global--spacer--sm);
margin-block-start: 0;
}
}
&.pf-c-login__main-footer-band {
& > *:last-child {
padding-block-end: var(--pf-c-login__main-footer-band-item--PaddingTop);
}
}
&.pf-c-modal-box__footer {
--ak-fieldset__legend--PaddingInlineBase: var(--pf-global--spacer--lg);
--pf-c-modal-box__footer--c-button--MarginRight: 0;
gap: var(--pf-global--spacer--sm);
justify-content: end;
padding-block: calc(var(--ak-fieldset__legend--PaddingInlineBase) / 2);
border-inline: none;
border-block-end: none;
--pf-c-modal-box__footer--c-button--sm--MarginRight: var(
--pf-c-modal-box__footer--c-button--MarginRight
);
& > ak-spinner-button:not(:last-child) {
margin-right: var(--pf-c-modal-box__footer--c-button--MarginRight);
}
}
}
/* #endregion */
/* #region Radio */
.pf-c-radio {
@@ -402,21 +322,6 @@ ak-switch-input {
border-bottom: 0;
}
fieldset {
border-color: var(
--ak-fieldset--BorderColor,
var(--pf-global--BackgroundColor--dark-transparent-200)
);
@media (prefers-contrast: more) {
border-color: var(--ak-fieldset--BorderColor, var(--pf-global--BorderColor--300));
}
@media (prefers-contrast: less) {
border-color: var(--ak-fieldset--BorderColor, transparent);
}
}
/* #endregion */
/* #region Radio */

View File

@@ -16,13 +16,13 @@
position: relative;
}
@media (width >= 1200px) {
@media (width > 1210px) {
.pf-c-page__sidebar.pf-m-expanded {
--pf-c-page__sidebar--BoxShadow: var(--pf-global--BoxShadow--sm-right);
}
}
@media (width < 1200px) {
@media (width <= 1210px) {
.pf-c-page__sidebar.pf-m-expanded ~ .pf-c-page__drawer .pf-c-page__sidebar-backdrop::after {
background-color: var(--pf-global--BackgroundColor--dark-transparent-100);
content: "";

View File

@@ -17,7 +17,7 @@
}
.pf-c-wizard__main-body {
--ak-fieldset--BorderColor: var(--pf-global--BackgroundColor--150);
--ak-c-fieldset--BorderColor: var(--pf-global--BackgroundColor--150);
gap: var(--pf-global--spacer--lg);
@@ -44,6 +44,8 @@
.pf-c-wizard__footer {
justify-content: end;
align-items: center;
/** Approximation of the height of navigation buttons to avoid excessive layout shifts when they are added or removed. */
min-height: calc(var(--pf-global--spacer--3xl) + (var(--pf-global--spacer--form-element) * 2));
}
.pf-c-wizard__nav-link {

View File

@@ -9,6 +9,7 @@
@import "@patternfly/patternfly/utilities/Text/text.css";
@import "./components/Drawer/drawer.css";
@import "./components/Form/form.css";
@import "./components/Fieldset/fieldset.css";
@import "./components/Login/login.css";
@import "./components/Icon/icon.css";
@import "#elements/locale/ak-locale-select.css";

View File

@@ -132,7 +132,7 @@ ak-app-icon {
[part="app-group-header"] {
@media not (prefers-contrast: more) {
--ak-fieldset__legend--PaddingInlineBase: 1rem;
--ak-c-fieldset__legend--PaddingInlineBase: 1rem;
padding-block-start: 0 !important;
padding-inline: 0 !important;
margin-inline: 0 !important;

View File

@@ -63,6 +63,7 @@ export const AKLibraryApplicationList: LitFC<AKLibraryApplicationListProps> = ({
const groupID = kebabCase(groupLabel);
return html`<fieldset
class="ak-c-fieldset"
data-group-id=${ifPresent(groupID)}
part="app-group"
data-group-index=${groupIndex}

View File

@@ -178,7 +178,7 @@ export class LibraryPage extends WithSession(AKElement) {
threshold: 0.3,
});
public pageTitle = msg("User Dashboard - Applications");
public pageTitle = msg("My Applications");
//#region Lifecycle
@@ -432,7 +432,7 @@ export class LibraryPage extends WithSession(AKElement) {
protected override render() {
return html`<div class="pf-c-page__main">
<div class="pf-c-page__header pf-c-content">
<h1 class="pf-c-page__title">${msg("User Dashboard")}</h1>
<h1 class="pf-c-page__title">${msg("My applications")}</h1>
${this.searchEnabled ? this.renderSearch() : nothing}
</div>
<main

View File

@@ -93,7 +93,7 @@ export class LibraryPage extends AKElement {
);
}
public pageTitle = msg("User Dashboard - Applications");
public pageTitle = msg("My Applications");
render() {
if (this.apps.loading) {

View File

@@ -18,7 +18,7 @@ test.describe("Session management", () => {
page.getByRole("heading", {
level: 1,
}),
).toHaveText("User Dashboard", {
).toHaveText("My applications", {
timeout: 10_000,
});
});

View File

@@ -39,6 +39,7 @@
"aria-description",
"inert",
"enterkeyhint",
//#endregion

View File

@@ -1302,10 +1302,6 @@
<source>Open in new tab</source>
<target>Otevřít v nové záložce</target>
</trans-unit>
<trans-unit id="s8655c52824caac63">
<source>If checked, the launch URL will open in a new browser tab or window from the user's application library.</source>
<target>Pokud je zaškrtnuto, spouštěcí URL se otevře v nové záložce nebo okně prohlížeče z knihovny aplikací uživatele.</target>
</trans-unit>
<trans-unit id="s909e876731a8febb">
<source>Select all rows</source>
<target>Vybrat všechny řádky</target>
@@ -1393,18 +1389,10 @@
<source>Policy</source>
<target>Zásada</target>
</trans-unit>
<trans-unit id="s6b85380416964890">
<source>Negate result</source>
<target>Negovat výsledek</target>
</trans-unit>
<trans-unit id="s3bfa0258999fb629">
<source>Negates the outcome of the binding. Messages are unaffected.</source>
<target>Neguje výsledek vazby. Zprávy nejsou ovlivněny.</target>
</trans-unit>
<trans-unit id="s6b1ed7507f26cb4a">
<source>Failure result</source>
<target>Výsledek selhání</target>
</trans-unit>
<trans-unit id="sfdfa5bb4ddd99d70">
<source>Enterprise only</source>
<target>Pouze podnik</target>
@@ -6143,10 +6131,6 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
<source>Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.</source>
<target>Krok používaný k ověření jakéhokoli autentikátoru. Tento krok by měl být použit během ověřovacích nebo autorizačních toků.</target>
</trans-unit>
<trans-unit id="s73c13e5a6f5e38a3">
<source>Device classes</source>
<target>Třídy zařízení</target>
</trans-unit>
<trans-unit id="sd8d9451f86502d1a">
<source>Device classes which can be used to authenticate.</source>
<target>Třídy zařízení, které lze použít k ověření.</target>
@@ -6391,10 +6375,6 @@ neprojde, když jedna nebo obě z vybraných možností jsou rovny nebo nad prah
<source>Let the user identify themselves with their username or Email address.</source>
<target>Umožnit uživateli identifikovat se uživatelským jménem nebo emailovou adresou.</target>
</trans-unit>
<trans-unit id="s592ab7d2bc1b8973">
<source>User fields</source>
<target>Uživatelská pole</target>
</trans-unit>
<trans-unit id="s4cdae7635e757555">
<source>Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.</source>
<target>Pole, kterými se uživatel může identifikovat. Pokud nejsou vybrána žádná pole, uživatel bude moci používat pouze zdroje.</target>
@@ -8608,10 +8588,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="s45915ee0293ce5d4">
<source>Ungrouped</source>
</trans-unit>
<trans-unit id="s1cf2298d92c327a6">
<source>My Applications</source>
<target>Moje aplikace</target>
</trans-unit>
<trans-unit id="s65b433c52c2ad8eb">
<source>Search for an application by name...</source>
</trans-unit>
@@ -8619,10 +8595,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<source>Search returned no results.</source>
<target>Hledání nevrátilo žádné výsledky.</target>
</trans-unit>
<trans-unit id="s2656433a3b1f7e86">
<source>My applications</source>
<target>Moje aplikace</target>
</trans-unit>
<trans-unit id="s91cfe4fac0957ee7">
<source>Application list</source>
</trans-unit>
@@ -10959,12 +10931,6 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
<trans-unit id="s9eda7101f63a8652">
<source>Hide from My applications</source>
</trans-unit>
<trans-unit id="s30f30e9c42594a33">
<source>If checked, this application will not be shown on the user's My applications page.</source>
</trans-unit>
<trans-unit id="s2ea2e39e4b470249">
<source>EntityID/Issuer override</source>
</trans-unit>
@@ -11098,6 +11064,33 @@ Vazby na skupiny/uživatele jsou kontrolovány vůči uživateli události.</tar
<trans-unit id="sdcd1a9744efdbd7e">
<source>Choose Policy Type</source>
</trans-unit>
<trans-unit id="sf4eb7c0c8e92e6b2">
<source>Whether the launch URL will open in a new browser tab or window from the user's application library.</source>
</trans-unit>
<trans-unit id="s8ab0176c9cf77b1a">
<source>Hide from User Dashboard</source>
</trans-unit>
<trans-unit id="s0b5847edb7150911">
<source>Whether this application will be shown on the User Dashboard.</source>
</trans-unit>
<trans-unit id="saa5ed8446baaba70">
<source>Negate Result</source>
</trans-unit>
<trans-unit id="s5387aa645962312a">
<source>Failure Result</source>
</trans-unit>
<trans-unit id="sea62de98f2e25e03">
<source>Device Classes</source>
</trans-unit>
<trans-unit id="s41938ae69656ef53">
<source>User Fields</source>
</trans-unit>
<trans-unit id="s977b2dff8b9def14">
<source>User Dashboard - Applications</source>
</trans-unit>
<trans-unit id="s190cbdd5b62e4746">
<source>User Dashboard</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1313,10 +1313,6 @@
<source>Open in new tab</source>
<target>Im neuen Tab öffnen</target>
</trans-unit>
<trans-unit id="s8655c52824caac63">
<source>If checked, the launch URL will open in a new browser tab or window from the user's application library.</source>
<target>Wenn diese Option aktiviert ist, wird die Aufruf-URL in einer neuen Browser-Registerkarte oder einem neuen Fenster der Anwendungsbibliothek des Benutzers geöffnet.</target>
</trans-unit>
<trans-unit id="s909e876731a8febb">
<source>Select all rows</source>
<target>Wählen Sie alle Zeilen aus</target>
@@ -1404,18 +1400,10 @@
<source>Policy</source>
<target>Richtlinie</target>
</trans-unit>
<trans-unit id="s6b85380416964890">
<source>Negate result</source>
<target>Ergebnis verneinen</target>
</trans-unit>
<trans-unit id="s3bfa0258999fb629">
<source>Negates the outcome of the binding. Messages are unaffected.</source>
<target>Negiert das Ergebnis der Bindung. Nachrichten sind nicht betroffen.</target>
</trans-unit>
<trans-unit id="s6b1ed7507f26cb4a">
<source>Failure result</source>
<target>Fehlergebnis</target>
</trans-unit>
<trans-unit id="sfdfa5bb4ddd99d70">
<source>Enterprise only</source>
<target>Enterprise-Feature</target>
@@ -6169,10 +6157,6 @@ Beim Erstellen eines festen Auswahlfelds aktiviere „Als Ausdruck interpretiere
<source>Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.</source>
<target>Stage, die verwendet wird, um einen beliebigen Authentifikator zu validieren. Diese Stage sollte während Authentifizierungs- oder Autorisierungs-Flows verwendet werden.</target>
</trans-unit>
<trans-unit id="s73c13e5a6f5e38a3">
<source>Device classes</source>
<target>Geräteklassen</target>
</trans-unit>
<trans-unit id="sd8d9451f86502d1a">
<source>Device classes which can be used to authenticate.</source>
<target>Geräteklassen, die zur Authentifizierung verwendet werden können.</target>
@@ -6417,10 +6401,6 @@ Beim Erstellen eines festen Auswahlfelds aktiviere „Als Ausdruck interpretiere
<source>Let the user identify themselves with their username or Email address.</source>
<target>Lassen Sie den Benutzer sich mit seinem Benutzernamen oder seiner E-Mail-Adresse identifizieren.</target>
</trans-unit>
<trans-unit id="s592ab7d2bc1b8973">
<source>User fields</source>
<target>Benutzerfelder</target>
</trans-unit>
<trans-unit id="s4cdae7635e757555">
<source>Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.</source>
<target>Felder, mit denen sich ein Benutzer identifizieren kann. Wenn keine Felder ausgewählt sind, kann der Benutzer nur Quellen verwenden.</target>
@@ -8640,10 +8620,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="s45915ee0293ce5d4">
<source>Ungrouped</source>
</trans-unit>
<trans-unit id="s1cf2298d92c327a6">
<source>My Applications</source>
<target>Meine Anwendungen</target>
</trans-unit>
<trans-unit id="s65b433c52c2ad8eb">
<source>Search for an application by name...</source>
</trans-unit>
@@ -8651,10 +8627,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<source>Search returned no results.</source>
<target>Suche ergab keine Treffer.</target>
</trans-unit>
<trans-unit id="s2656433a3b1f7e86">
<source>My applications</source>
<target>Meine Anwendungen</target>
</trans-unit>
<trans-unit id="s91cfe4fac0957ee7">
<source>Application list</source>
</trans-unit>
@@ -10991,12 +10963,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
<trans-unit id="s9eda7101f63a8652">
<source>Hide from My applications</source>
</trans-unit>
<trans-unit id="s30f30e9c42594a33">
<source>If checked, this application will not be shown on the user's My applications page.</source>
</trans-unit>
<trans-unit id="s2ea2e39e4b470249">
<source>EntityID/Issuer override</source>
</trans-unit>
@@ -11130,6 +11096,33 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
<trans-unit id="sdcd1a9744efdbd7e">
<source>Choose Policy Type</source>
</trans-unit>
<trans-unit id="sf4eb7c0c8e92e6b2">
<source>Whether the launch URL will open in a new browser tab or window from the user's application library.</source>
</trans-unit>
<trans-unit id="s8ab0176c9cf77b1a">
<source>Hide from User Dashboard</source>
</trans-unit>
<trans-unit id="s0b5847edb7150911">
<source>Whether this application will be shown on the User Dashboard.</source>
</trans-unit>
<trans-unit id="saa5ed8446baaba70">
<source>Negate Result</source>
</trans-unit>
<trans-unit id="s5387aa645962312a">
<source>Failure Result</source>
</trans-unit>
<trans-unit id="sea62de98f2e25e03">
<source>Device Classes</source>
</trans-unit>
<trans-unit id="s41938ae69656ef53">
<source>User Fields</source>
</trans-unit>
<trans-unit id="s977b2dff8b9def14">
<source>User Dashboard - Applications</source>
</trans-unit>
<trans-unit id="s190cbdd5b62e4746">
<source>User Dashboard</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -998,9 +998,6 @@
<trans-unit id="s2348f46ebf436671">
<source>Open in new tab</source>
</trans-unit>
<trans-unit id="s8655c52824caac63">
<source>If checked, the launch URL will open in a new browser tab or window from the user's application library.</source>
</trans-unit>
<trans-unit id="s909e876731a8febb">
<source>Select all rows</source>
</trans-unit>
@@ -1064,15 +1061,9 @@
<trans-unit id="s042baf59902a711f">
<source>Policy</source>
</trans-unit>
<trans-unit id="s6b85380416964890">
<source>Negate result</source>
</trans-unit>
<trans-unit id="s3bfa0258999fb629">
<source>Negates the outcome of the binding. Messages are unaffected.</source>
</trans-unit>
<trans-unit id="s6b1ed7507f26cb4a">
<source>Failure result</source>
</trans-unit>
<trans-unit id="sfdfa5bb4ddd99d70">
<source>Enterprise only</source>
</trans-unit>
@@ -4748,9 +4739,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s0e15f678445dfc45">
<source>Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.</source>
</trans-unit>
<trans-unit id="s73c13e5a6f5e38a3">
<source>Device classes</source>
</trans-unit>
<trans-unit id="sd8d9451f86502d1a">
<source>Device classes which can be used to authenticate.</source>
</trans-unit>
@@ -4934,9 +4922,6 @@ doesn't pass when either or both of the selected options are equal or above the
<trans-unit id="s4af8a3ce5a600855">
<source>Let the user identify themselves with their username or Email address.</source>
</trans-unit>
<trans-unit id="s592ab7d2bc1b8973">
<source>User fields</source>
</trans-unit>
<trans-unit id="s4cdae7635e757555">
<source>Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.</source>
</trans-unit>
@@ -6632,18 +6617,12 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s45915ee0293ce5d4">
<source>Ungrouped</source>
</trans-unit>
<trans-unit id="s1cf2298d92c327a6">
<source>My Applications</source>
</trans-unit>
<trans-unit id="s65b433c52c2ad8eb">
<source>Search for an application by name...</source>
</trans-unit>
<trans-unit id="s4facec1106c91cf9">
<source>Search returned no results.</source>
</trans-unit>
<trans-unit id="s2656433a3b1f7e86">
<source>My applications</source>
</trans-unit>
<trans-unit id="s91cfe4fac0957ee7">
<source>Application list</source>
</trans-unit>
@@ -8965,12 +8944,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s71724d2ff6637203">
<source>Altered behavior for usage with VMware vCenter.</source>
</trans-unit>
<trans-unit id="s9eda7101f63a8652">
<source>Hide from My applications</source>
</trans-unit>
<trans-unit id="s30f30e9c42594a33">
<source>If checked, this application will not be shown on the user's My applications page.</source>
</trans-unit>
<trans-unit id="s2ea2e39e4b470249">
<source>EntityID/Issuer override</source>
</trans-unit>
@@ -9104,6 +9077,33 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sdcd1a9744efdbd7e">
<source>Choose Policy Type</source>
</trans-unit>
<trans-unit id="sf4eb7c0c8e92e6b2">
<source>Whether the launch URL will open in a new browser tab or window from the user's application library.</source>
</trans-unit>
<trans-unit id="s8ab0176c9cf77b1a">
<source>Hide from User Dashboard</source>
</trans-unit>
<trans-unit id="s0b5847edb7150911">
<source>Whether this application will be shown on the User Dashboard.</source>
</trans-unit>
<trans-unit id="saa5ed8446baaba70">
<source>Negate Result</source>
</trans-unit>
<trans-unit id="s5387aa645962312a">
<source>Failure Result</source>
</trans-unit>
<trans-unit id="sea62de98f2e25e03">
<source>Device Classes</source>
</trans-unit>
<trans-unit id="s41938ae69656ef53">
<source>User Fields</source>
</trans-unit>
<trans-unit id="s977b2dff8b9def14">
<source>User Dashboard - Applications</source>
</trans-unit>
<trans-unit id="s190cbdd5b62e4746">
<source>User Dashboard</source>
</trans-unit>
</body>
</file>
</xliff>

Some files were not shown because too many files have changed in this diff Show More