Compare commits

...

84 Commits

Author SHA1 Message Date
Teffen Ellis
59bf2b143e web: Port Captcha field validation. 2025-07-25 17:51:34 +02:00
Jens L.
19980d05e8 website: Fix cross domain search, port backwards-compatible Netlify setup (#15775)
* website: remove docs ID so cross domain search works again

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

* website: Port ignorefile.

* website: Port dependencies grouping.

* website: Port deployment path.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
2025-07-25 13:46:38 +02:00
Tana M Berry
b53c004496 website: Flesh out Makefile commands, usage. (cherry-pick #15576) (#15728)
website: Flesh out Makefile commands, usage. (#15576)

* website: Flesh out command behavior.

* restructure

* rearranged

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-07-22 14:36:21 -05:00
Jens Langhammer
23ffad1c6b release: 2025.6.4 2025-07-22 14:23:32 +02:00
Jens L
ce3f9e3763 security: fix CVE-2025-53942 (#15719)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	authentik/stages/user_login/stage.py
2025-07-22 14:22:54 +02:00
gcp-cherry-pick-bot[bot]
4b0402b51a providers/radius: set message authenticator (cherry-pick #15635) (#15665)
providers/radius: set message authenticator (#15635)

* core: fix flow planner checking against wrong user when creating recovery link



* validate incoming message authenticator



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-07-19 22:40:55 +02:00
gcp-cherry-pick-bot[bot]
974bd6665d website/docs: fix user ref typos (cherry-pick #15653) (#15657)
website/docs: fix user ref typos (#15653)

Fixed typos

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-07-18 22:18:13 +02:00
gcp-cherry-pick-bot[bot]
1cff6fdd67 website/docs: added enterprise label to new Logging docs (cherry-pick #15556) (#15573)
website/docs: added enterprise label to new Logging docs (#15556)

added enterprise label

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-07-15 16:15:25 +02:00
gcp-cherry-pick-bot[bot]
70845b1d5c website/docs: fix a typo in SSF docs (cherry-pick #15554) (#15555) 2025-07-14 19:32:45 +02:00
gcp-cherry-pick-bot[bot]
7a39d8ce64 website/docs: add use case, move diagram, link to ABM (cherry-pick #15491) (#15549)
website/docs: add use case, move diagram, link to ABM (#15491)

* add use case, move diagram, link to ABM

* change word to match

* fix UI



* Update website/docs/add-secure-apps/flows-stages/stages/authenticator_endpoint_gdtc/index.md




---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-07-14 10:38:15 -05:00
gcp-cherry-pick-bot[bot]
12682bf08e website/docs: Add FIDO2 references to the documentation (cherry-pick #14826) (#15521)
website/docs: Add FIDO2 references to the documentation (#14826)

* Add FIDO2 references to the documentation

* Update website/docs/add-secure-apps/flows-stages/stages/authenticator_webauthn/index.mdx




---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-07-11 13:20:11 -05:00
gcp-cherry-pick-bot[bot]
86f430b7b1 website/docs: add noun for SSO (cherry-pick #15509) (#15512)
website/docs: add noun for SSO (#15509)

* add noun for SSO

* change to use term platform

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-07-10 18:24:19 -05:00
Marc 'risson' Schmitt
f57d345c9a providers/proxy: fix ingress-nginx proxy buffer size annotations (cherry-pick #15506) (#15507) 2025-07-10 17:15:11 +02:00
gcp-cherry-pick-bot[bot]
41e2106d9f website/docs: troubleshooting: Fix variable for postgres database in k8s (cherry-pick #15503) (#15505)
Co-authored-by: Dominic R <dominic@sdko.org>
Fix variable for postgres database in k8s (#15503)
2025-07-10 16:41:54 +02:00
gcp-cherry-pick-bot[bot]
467a26faf5 website/docs: postgres troubleshooting: get PGPASSWORD from POSTGRES_PASSWORD_FILE (cherry-pick #15039) (#15500)
Co-authored-by: Maciek <vonProteus@users.noreply.github.com>
2025-07-10 13:52:47 +02:00
Tana M Berry
1525a4c2e5 website/docs: Cherry pick events docs (cherry-pick #15457) (#15461)
website/docs: edits to latest Events docs (#15457)

* edits to latest Events docs

* Optimised images with calibre/image-actions

---------



# Conflicts:
#	website/docs/sys-mgmt/events/event-actions.md
#	website/docs/sys-mgmt/events/events-diffs.png
#	website/docs/sys-mgmt/events/index.md
#	website/docs/sys-mgmt/events/logging-events.md
#	website/docs/sys-mgmt/events/notifications.md
#	website/docs/sys-mgmt/events/transports.md

Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-07-08 21:07:25 +02:00
gcp-cherry-pick-bot[bot]
9645bdc9b9 website/docs: improve the docs about our Events and logging (cherry-pick #15270) (#15463)
website/docs: improve the docs about our Events and logging (#15270)

* tweak

* more content

* major surgery

* fix image link

* Optimised images with calibre/image-actions

* tweaks

* dom and dewi edits

* tweak to bump build

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-07-08 21:02:34 +02:00
gcp-cherry-pick-bot[bot]
952edb75fd website/docs: add manual RAC outpost deployment information (cherry-pick #15362) (#15433)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-07-08 15:02:19 +02:00
gcp-cherry-pick-bot[bot]
4b73a1ec38 core: fix set_token_key permission not declared (cherry-pick #15384) (#15391)
core: fix set_token_key permission not declared (#15384)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-07-03 22:13:30 +02:00
Jens L
600052bd0f web/admin: fix nested table pagination and search (#15385)
* web/admin: fix nested table pagination and search

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

* update

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/src/admin/events/EventListPage.ts
#	web/src/elements/table/Table.ts
2025-07-03 21:00:41 +02:00
gcp-cherry-pick-bot[bot]
5d5fb39736 website: Mark all URLs as external. (cherry-pick #15363) (#15373)
website: Mark all URLs as external. (#15363)

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-07-03 16:47:01 +02:00
gcp-cherry-pick-bot[bot]
cc7b25d828 core: fix missing serializer on AuthenticatedSession (cherry-pick #15323) (#15365)
core: fix missing serializer on AuthenticatedSession (#15323)

fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-07-02 18:40:13 +02:00
gcp-cherry-pick-bot[bot]
a8134e26c8 website/docs: re-add gtag (cherry-pick #15334) (#15335)
website/docs: re-add gtag (#15334)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-07-01 19:33:23 +02:00
gcp-cherry-pick-bot[bot]
1c637bc0d1 web/elements: fix table search not resetting page when query changes (cherry-pick #15324) (#15326)
web/elements: fix table search not resetting page when query changes (#15324)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-07-01 17:18:43 +02:00
Jens L
8af668a9d2 website: changelog for security releases (#15291)
* website: changelog for security releases

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2025/v2025.4.md
2025-06-27 15:43:14 +02:00
Jens Langhammer
96266e2e2b release: 2025.6.3 2025-06-27 15:34:53 +02:00
gcp-cherry-pick-bot[bot]
65373ab217 security: fix CVE-2025-52553 (cherry-pick #15289) (#15290)
security: fix CVE-2025-52553 (#15289)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-27 15:30:42 +02:00
Marcelo Elizeche Landó
88590e1134 core: bump goauthentik/fips-python from 3.13.3-slim-bookworm-fips to 3.13.5-slim-bookworm-fips in 2025.6 (#15274)
core: bump goauthentik/fips-python from 3.13.3-slim-bookworm-fips to 3.13.4-slim-bookworm-fips in 2025.6

Signed-off-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-27 00:43:45 +02:00
gcp-cherry-pick-bot[bot]
f6ff31e3de web/elements: typing error when variables are not converted to string (cherry-pick #15169) (#15222)
web/elements: typing error when variables are not converted to string (#15169)

fix: typing error when variables are not converted to string

Co-authored-by: Leandro4002 <leandro262009@gmail.com>
Co-authored-by: leandro.saraiva <leandro.saraiva@adonite.com>
2025-06-24 20:24:49 +02:00
gcp-cherry-pick-bot[bot]
cbc14524b3 website/docs: Add steps to troubleshoot /initial-setup/ (cherry-pick #15011) (#15210)
website/docs: Add steps to troubleshoot /initial-setup/ (#15011)

* Add steps to troubleshoot /initial-setup/

* fix linting

* Apply suggestions from code review




* Apply suggestions from code review




* add email part

* Apply suggestions from code review




* Apply suggestions from code review




* change wording

---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-06-24 00:43:56 +02:00
gcp-cherry-pick-bot[bot]
d807038d05 website/docs: fix note at end of rac credentials prompt (cherry-pick #14909) (#15208)
website/docs: fix note at end of rac credentials prompt (#14909)

* Fixes note section at end of document

* tweak to bump build

---------

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-23 22:17:59 +02:00
gcp-cherry-pick-bot[bot]
ba131a50ba website/docs: add credentials prompt for rac doc (cherry-pick #14840) (#15207)
website/docs: add credentials prompt for rac doc (#14840)

* Adds document

* Typo

* Clarified RAC endpoint sentence based on Tana's suggestion.

* Update website/docs/add-secure-apps/providers/rac/rac_credentials_prompt.md




* Small wording improvements

* Added connection security type information

* A word

* Added to sidebar

* Update website/docs/add-secure-apps/providers/rac/rac_credentials_prompt.md




* Applied suggestions from Tana

* Update website/docs/add-secure-apps/providers/rac/rac_credentials_prompt.md




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-23 21:15:58 +02:00
gcp-cherry-pick-bot[bot]
0e37a66751 ci: fix CodeQL failing on cherry-pick PRs (cherry-pick #15205) (#15206)
* ci: fix CodeQL failing on cherry-pick PRs (#15205)

* fix build

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-23 20:41:33 +02:00
Jens L
57c220dfc2 website: split integrations partially (#15076)
* config for split

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

* update alllll the links

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

* add redirect

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

* add separate job for integrations build

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

* Update website/netlify.toml

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Jens L. <jens@beryju.org>

* Revert "update alllll the links"

This reverts commit 872c5870a8.

* absolute relative URLs only

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

* but use a plugin to rewrite them

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

* fix external URL regex

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

* make rewrite plugin more re-usable

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

* fix the reverse links

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

* lint

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

* fix root redirect

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

* fix rediret

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

* fix root redirect

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

* fix redirect

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	.github/workflows/ci-website.yml
#	website/docusaurus.config.esm.mjs
2025-06-23 15:53:08 +02:00
gcp-cherry-pick-bot[bot]
7339a22080 web/user: fix infinite loop when no user settings flow is set (cherry-pick #15188) (#15192)
web/user: fix infinite loop when no user settings flow is set (#15188)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-23 01:50:38 +02:00
gcp-cherry-pick-bot[bot]
8c2d72affe website/docs: fix egregious maintenance fail (cherry-pick #15176) (#15180)
website/docs: fix egregious maintenance fail (#15176)

fix egregious maintenance fail

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-22 00:58:35 +02:00
gcp-cherry-pick-bot[bot]
9c31e08bd9 website/docs: fixes misplaced sentence (cherry-pick #14998) (#15182)
website/docs: fixes misplaced sentence (#14998)

fixes misplaced sentence

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-22 00:58:16 +02:00
Jens L
805b9afa80 stages/user_login: fix session binding logging (#15175)
* add tests

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

* fix logging

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

* update test db?

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

* ah there we go; fix mmdb not being reloaded with test settings

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	authentik/root/test_runner.py
2025-06-21 00:23:10 +02:00
gcp-cherry-pick-bot[bot]
bc809bae1e core: bump protobuf from 6.30.2 to v6.31.1 (cherry-pick #14894) (#15173)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-20 14:50:10 +02:00
gcp-cherry-pick-bot[bot]
66773b69ab core: bump urllib3 from 2.4.0 to v2.5.0 (cherry-pick #15131) (#15174)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-20 14:49:59 +02:00
gcp-cherry-pick-bot[bot]
c149701501 sources/ldap: fix sync on empty groups (cherry-pick #15158) (#15171)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
fix sync on empty groups (#15158)
2025-06-20 14:25:55 +02:00
gcp-cherry-pick-bot[bot]
b13b51b73a core: bump requests from 2.32.3 to v2.32.4 (cherry-pick #15129) (#15135)
core: bump requests from 2.32.3 to v2.32.4 (#15129)

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-19 00:23:05 +02:00
gcp-cherry-pick-bot[bot]
ee30cc1ede core: bump tornado from 6.4.2 to v6.5.1 (cherry-pick #15100) (#15116)
core: bump tornado from 6.4.2 to v6.5.1 (#15100)

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-18 18:48:02 +02:00
gcp-cherry-pick-bot[bot]
de095f4a10 web/elements: Add light mode custom css handling (cherry-pick #14944) (#15096)
web/elements: Add light mode custom css handling (#14944)

* fix(ui): Add light mode custom css handling



* Update Base.ts



---------

Signed-off-by: CodeMax IT Solutions Pvt. Ltd. <137166088+cdmx1@users.noreply.github.com>
Co-authored-by: CodeMax IT Solutions Pvt. Ltd. <137166088+cdmx1@users.noreply.github.com>
2025-06-17 20:31:08 +02:00
gcp-cherry-pick-bot[bot]
abf3aa3a7f ci: fix post-release e2e builds failing (cherry-pick #15082) (#15092)
ci: fix post-release e2e builds failing (#15082)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-17 09:37:26 +02:00
gcp-cherry-pick-bot[bot]
3ebae72a76 website/docs: add more to style guide (cherry-pick #14797) (#15077)
website/docs: add more to style guide (#14797)

* lists and variables

* lists and variables

* tweaks

* kens edit

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-16 22:15:37 +02:00
Simonyi Gergő
b6b47b669e release: 2025.6.2 2025-06-16 18:20:22 +02:00
gcp-cherry-pick-bot[bot]
8422568c42 website/docs: release notes for 2025.6.2 (cherry-pick #15065) (#15069)
website/docs: release notes for `2025.6.2` (#15065)

* website/docs: release notes for `2025.6.2`

* fixup! website/docs: release notes for `2025.6.2`

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-06-16 18:19:24 +02:00
Simonyi Gergő
9973064f50 core: fix transaction test case (cherry-pick #15021) (#15070)
* core: fix transaction test case (#15021)

* move patched ct to root

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

* use our transaction test case as base

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

* fix...?

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

* well apparently that works

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

---------

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

* fixup! core: fix transaction test case (#15021)

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-16 17:55:12 +02:00
gcp-cherry-pick-bot[bot]
4fea65f5cc website/docs: remove commented out config options (cherry-pick #15064) (#15067)
website/docs: remove commented out config options (#15064)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-16 16:56:45 +02:00
gcp-cherry-pick-bot[bot]
784446a47d website/docs: correct minor version in release notes (cherry-pick #15012) (#15068)
website/docs: correct minor version in release notes (#15012)

Signed-off-by: Zeik0s <35345686+Zeik0s@users.noreply.github.com>
Co-authored-by: Zeik0s <35345686+Zeik0s@users.noreply.github.com>
2025-06-16 16:47:44 +02:00
gcp-cherry-pick-bot[bot]
516bc65fc4 web/elements: fix typo in localeComparator (cherry-pick #15054) (#15055)
web/elements: fix typo in localeComparator (#15054)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-16 01:57:26 +02:00
gcp-cherry-pick-bot[bot]
efb59adeff providers/rac: fixes prompt data not being merged with connection_settings (cherry-pick #15037) (#15038)
providers/rac: fixes prompt data not being merged with connection_settings (#15037)

* Fixes line that pulls in prompt data

* fallback to old settings



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-06-13 23:28:44 +02:00
gcp-cherry-pick-bot[bot]
43a2ad66f0 website/docs: also hide the postgres pool_options setting (cherry-pick #15023) (#15032)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-06-13 15:40:49 +02:00
gcp-cherry-pick-bot[bot]
ec0c59f1fc core: bump django from 5.1.10 to 5.1.11 (cherry-pick #14997) (#15010)
core: bump django from 5.1.10 to 5.1.11 (#14997)

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-11 18:25:26 +02:00
gcp-cherry-pick-bot[bot]
8f80072321 core: bump django from 5.1.9 to 5.1.10 (cherry-pick #14951) (#15008)
core: bump django from 5.1.9 to 5.1.10 (#14951)

bump django to 5.1.10

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-11 18:09:04 +02:00
gcp-cherry-pick-bot[bot]
33f95c837b brands: fix custom_css being escaped (cherry-pick #14994) (#14996)
brands: fix custom_css being escaped (#14994)

* brands: fix custom_css being escaped



* escape adequately



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-10 17:21:22 +02:00
gcp-cherry-pick-bot[bot]
43637b8a75 internal/outpost: fix incorrect usage of golang SHA API (cherry-pick #14981) (#14982)
internal/outpost: fix incorrect usage of golang SHA API (#14981)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-10 00:28:20 +02:00
gcp-cherry-pick-bot[bot]
7a4518be26 web/elements: fix dual select without sortBy (cherry-pick #14977) (#14979)
web/elements: fix dual select without sortBy (#14977)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-09 19:48:19 +02:00
gcp-cherry-pick-bot[bot]
b94fb53821 stages/email: Only attach logo to email if used (cherry-pick #14835) (#14969)
stages/email: Only attach logo to email if used (#14835)

* Only attach logo to email if used

* Fix MIME logo attachment to adhere to standard

* format, fix web



* not tuple



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Christian Elsen <chriselsen@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-06-09 03:53:51 +02:00
gcp-cherry-pick-bot[bot]
2be5c9633b website/docs: add 2025.6.1 release notes (cherry-pick #14948) (#14949)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-06-06 17:28:58 +02:00
Marc 'risson' Schmitt
e729e42595 release: 2025.6.1 2025-06-06 16:55:05 +02:00
Marc 'risson' Schmitt
01d591b84e root: fix bumpversion
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-06-06 16:53:26 +02:00
gcp-cherry-pick-bot[bot]
dd08e1bf66 providers/proxy: add option to override host header with property mappings (cherry-pick #14927) (#14945)
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-06 15:27:10 +02:00
gcp-cherry-pick-bot[bot]
150705f221 web/user: fix user settings flow not loading (cherry-pick #14911) (#14930)
web/user: fix user settings flow not loading (#14911)

* web/user: fix user settings flow not loading



* fix



* unrelated fix: fix select caret color in dark theme



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-06-05 23:36:40 +02:00
Marc 'risson' Schmitt
6b39f6495e tenants: fix tenant aware celery scheduler (cherry-pick #14921) 2025-06-05 15:15:34 +02:00
gcp-cherry-pick-bot[bot]
639c57245b website/docs: rotate supported versions: 2025.6 (cherry-pick #14856) (#14906)
website/docs: rotate supported versions: `2025.6` (#14856)

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-06-04 18:46:58 +02:00
gcp-cherry-pick-bot[bot]
730600aea4 website/integrations: tailscale (cherry-pick #14499) (#14908)
website/integrations: tailscale (#14499)

* init

* wording

* lint

* Update website/integrations/services/tailscale/index.md



* Dewi's suggestions

* still mention that its a placeholder

* fix



* Update website/integrations/services/tailscale/index.md




* mv to end



* indent

* Update website/integrations/services/tailscale/index.md




* Update website/integrations/services/tailscale/index.md



* tweak to bump build

* another tweak to bump build

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-04 18:37:53 +02:00
gcp-cherry-pick-bot[bot]
e15ce5a3f0 website/release notes: add tailscale to new integrations (cherry-pick #14859) (#14877)
website/release notes: add tailscale to new integrations (#14859)

* website/release notes: add tailscale to new integrations

### What

Adds Tailscale to the list of new integrations this release as it was merged like 5 minutes ago and technically 2025.6 isn't released just yet



* tweaks to bump build

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-04 17:06:01 +02:00
gcp-cherry-pick-bot[bot]
1fc91b004b website/releases: order new integrations alphabetically (cherry-pick #14850) (#14876)
website/releases: order new integrations alphabetically (#14850)

### What

Orders the 2025.6 release note's new integrations alphabetically. It just bothers me.

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-06-04 17:05:33 +02:00
Simonyi Gergő
644705e6fe release: 2025.6.0 2025-06-03 22:04:09 +02:00
gcp-cherry-pick-bot[bot]
ff8ef523db website/docs: finalize release notes for 2025.6 (cherry-pick #14854) (#14855)
website/docs: finalize release notes for `2025.6` (#14854)

* remove internal changes from release notes

* add late additions to release notes

* remove release candidate notice from `2025.6`

* rotate supported versions

* rotate releases in sidebar

* Revert "rotate supported versions"

This reverts commit eea9d03e1d.

I'd like to do the release tonight, but I can't merge this because it
needs a review from @teams/security. I'll open a separate PR for it.

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-06-03 22:01:40 +02:00
gcp-cherry-pick-bot[bot]
1051dd19ea providers/rac: apply ConnectionToken scoped-settings last (cherry-pick #14838) (#14853)
providers/rac: apply ConnectionToken scoped-settings last (#14838)

* providers/rac: apply ConnectionToken scoped-settings last



* fix tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-03 21:26:13 +02:00
gcp-cherry-pick-bot[bot]
04cb4fd267 lib/sync: fix static incorrect label of pages (cherry-pick #14851) (#14852)
lib/sync: fix static incorrect label of pages (#14851)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-03 21:26:09 +02:00
gcp-cherry-pick-bot[bot]
da9508f839 website/docs: add LDAP docs for forward deletion and memberUid (cherry-pick #14814) (#14848)
website/docs: add LDAP docs for forward deletion and `memberUid` (#14814)

* website/docs: add LDAP docs for forward deletion and `memberUid`

* reword LDAP docs



---------

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-06-03 19:31:04 +02:00
gcp-cherry-pick-bot[bot]
841a286a25 stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (cherry-pick #14801) (#14847)
stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#14801)

* stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs



* replace removed device type in tests

Android Authenticator with SafetyNet Attestation was removed from
blob.jwt in the previous commit

---------

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>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Simonyi Gergő <gergo@goauthentik.io>
2025-06-03 19:30:56 +02:00
gcp-cherry-pick-bot[bot]
63c48d7b99 core: bump goauthentik.io/api/v3 from 3.2025041.2 to 3.2025041.4 (cherry-pick #14809) (#14846)
core: bump goauthentik.io/api/v3 from 3.2025041.2 to 3.2025041.4 (#14809)

Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025041.2 to 3.2025041.4.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2025041.2...v3.2025041.4)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-version: 3.2025041.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>
2025-06-03 19:30:14 +02:00
gcp-cherry-pick-bot[bot]
5994fd2c61 core, web: update translations (cherry-pick #14800) (#14845)
core, web: update translations (#14800)

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>
2025-06-03 19:30:06 +02:00
gcp-cherry-pick-bot[bot]
5f745e682e website/integrations: Update Zammad SAML Instructions (cherry-pick #14774) (#14844)
website/integrations: Update Zammad SAML Instructions (#14774)

* Update Zammad SAML Instructions

I just configured Zammad 6.4.1 to work with Authentik 2025.4.1. There seem to have been some changes since these instructions were written. The Name ID Format cannot be left blank. The SSO URL and the logout URL were incorrect. I was getting an Error 422 from Zammad until I turned on signing assertions, so I conclude that is required and I wrote instructions for that. I saw some discussion online elsewhere that the `----BEGIN` and `---END` lines should be removed. I tested it both ways and it worked both ways. I wrote the instructions to keep those lines in because it seemed simplest and most intuitive.



* Incorporate separate instructions for certificate file




* Incorporate simplified copy/paste instructions




* Incoporate formatting change




* Incorporate formatting changes




* Removed reference to custom properties

* Capitalisation




* Formatting




* Formatting




* Updated language




* Update website/integrations/services/zammad/index.md




* Update website/integrations/services/zammad/index.md




* tweak to bump build

* bump build

* use bold font for UI labels

* my typo

* capitalization fix

---------

Signed-off-by: Paco Hope <pacohope@users.noreply.github.com>
Co-authored-by: Paco Hope <pacohope@users.noreply.github.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-03 19:29:59 +02:00
gcp-cherry-pick-bot[bot]
6f1b16e7f9 website/integrations: remove trailing slash from budibase redirect (cherry-pick #14823) (#14843)
website/integrations: remove trailing slash from budibase redirect (#14823)

Removes trailing slash from redirect

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-06-03 19:29:51 +02:00
gcp-cherry-pick-bot[bot]
57bce19e7a website/integrations: update cloudflare access callback url (cherry-pick #14807) (#14842)
website/integrations: update cloudflare access callback url (#14807)

Update CLoudflare Access index.md

The callback URL had a trailing / that breaks the callback URL being matched by a strict policy.

Signed-off-by: terafirmanz <53923271+terafirmanz@users.noreply.github.com>
Co-authored-by: terafirmanz <53923271+terafirmanz@users.noreply.github.com>
2025-06-03 19:29:40 +02:00
gcp-cherry-pick-bot[bot]
850c5d5a45 website/docs: remove fluff from release notes 2025.6 (cherry-pick #14819) (#14820)
remove fluff from release notes 2025.6 (#14819)

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-06-03 15:21:51 +02:00
gcp-cherry-pick-bot[bot]
8b7d11f94c web: minor design tweaks (cherry-pick #14803) (#14804)
web: minor design tweaks (#14803)

* fix spacing between header and page desc



* fix icon alignment



* fallback text when we dont have a user yet



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-01 23:15:08 +02:00
Simonyi Gergő
45737909f6 release: 2025.6.0-rc1 2025-05-31 00:44:04 +02:00
154 changed files with 2567 additions and 1484 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.4.1
current_version = 2025.6.4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@@ -21,6 +21,8 @@ optional_value = final
[bumpversion:file:package.json]
[bumpversion:file:package-lock.json]
[bumpversion:file:docker-compose.yml]
[bumpversion:file:schema.yml]
@@ -31,6 +33,4 @@ optional_value = final
[bumpversion:file:internal/constants/constants.go]
[bumpversion:file:web/src/common/constants.ts]
[bumpversion:file:lifecycle/aws/template.yaml]

View File

@@ -202,7 +202,7 @@ jobs:
uses: actions/cache@v4
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
- name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web

View File

@@ -49,6 +49,7 @@ jobs:
matrix:
job:
- build
- build:integrations
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

View File

@@ -2,7 +2,7 @@ name: "CodeQL"
on:
push:
branches: [main, "*", next, version*]
branches: [main, next, version*]
pull_request:
branches: [main]
schedule:

View File

@@ -96,7 +96,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.8 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \

View File

@@ -86,6 +86,10 @@ dev-create-db:
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
update-test-mmdb: ## Update test GeoIP and ASN Databases
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
#########################
## API Schema
#########################

View File

@@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
| Version | Supported |
| --------- | --------- |
| 2025.2.x | ✅ |
| 2025.4.x | ✅ |
| 2025.6.x | ✅ |
## Reporting a Vulnerability

View File

@@ -2,7 +2,7 @@
from os import environ
__version__ = "2025.4.1"
__version__ = "2025.6.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -5,7 +5,6 @@ from collections.abc import Callable
from django.apps import apps
from django.test import TestCase
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
from authentik.providers.oauth2.models import RefreshToken
@@ -22,10 +21,13 @@ def serializer_tester_factory(test_model: type[SerializerModel]) -> Callable:
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
# Models that have subclasses don't have to have a serializer
if len(test_model.__subclasses__()) > 0:
return
self.assertIsNotNone(model_class.serializer)
if model_class.serializer.Meta().model == RefreshToken:
return
self.assertEqual(model_class.serializer.Meta().model, test_model)
self.assertTrue(issubclass(test_model, model_class.serializer.Meta().model))
return tester
@@ -34,6 +36,6 @@ for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
if not issubclass(model, SerializerModel):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))

View File

@@ -148,3 +148,14 @@ class TestBrands(APITestCase):
"default_locale": "",
},
)
def test_custom_css(self):
"""Test custom_css"""
brand = create_test_brand()
brand.branding_custom_css = """* {
font-family: "Foo bar";
}"""
brand.save()
res = self.client.get(reverse("authentik_core:if-user"))
self.assertEqual(res.status_code, 200)
self.assertIn(brand.branding_custom_css, res.content.decode())

View File

@@ -5,6 +5,8 @@ from typing import Any
from django.db.models import F, Q
from django.db.models import Value as V
from django.http.request import HttpRequest
from django.utils.html import _json_script_escapes
from django.utils.safestring import mark_safe
from authentik import get_full_version
from authentik.brands.models import Brand
@@ -32,8 +34,13 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects brand object into every template"""
brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant())
# similarly to `json_script` we escape everything HTML-related, however django
# only directly exposes this as a function that also wraps it in a <script> tag
# which we dont want for CSS
brand_css = mark_safe(str(brand.branding_custom_css).translate(_json_script_escapes)) # nosec
return {
"brand": brand,
"brand_css": brand_css,
"footer_links": tenant.footer_links,
"html_meta": {**get_http_meta()},
"version": get_full_version(),

View File

@@ -5,6 +5,7 @@ from contextvars import ContextVar
from functools import partial
from uuid import uuid4
from django.contrib.auth import logout
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
@@ -58,6 +59,11 @@ class AuthenticationMiddleware(MiddlewareMixin):
request.user = SimpleLazyObject(lambda: get_user(request))
request.auser = partial(aget_user, request)
user = request.user
if user and user.is_authenticated and not user.is_active:
logout(request)
raise AssertionError()
class ImpersonateMiddleware:
"""Middleware to impersonate users"""

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.1.11 on 2025-07-03 13:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0048_delete_oldauthenticatedsession_content_type"),
]
operations = [
migrations.AlterModelOptions(
name="token",
options={
"permissions": [
("view_token_key", "View token's key"),
("set_token_key", "Set a token's key"),
],
"verbose_name": "Token",
"verbose_name_plural": "Tokens",
},
),
]

View File

@@ -953,7 +953,10 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
models.Index(fields=["identifier"]),
models.Index(fields=["key"]),
]
permissions = [("view_token_key", _("View token's key"))]
permissions = [
("view_token_key", _("View token's key")),
("set_token_key", _("Set a token's key")),
]
def __str__(self):
description = f"{self.identifier}"
@@ -1082,6 +1085,12 @@ class AuthenticatedSession(SerializerModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
@property
def serializer(self) -> type[Serializer]:
from authentik.core.api.authenticated_sessions import AuthenticatedSessionSerializer
return AuthenticatedSessionSerializer
class Meta:
verbose_name = _("Authenticated Session")
verbose_name_plural = _("Authenticated Sessions")

View File

@@ -16,7 +16,7 @@
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<style>{{ brand.branding_custom_css }}</style>
<style>{{ brand_css }}</style>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %}

View File

@@ -16,7 +16,7 @@ from authentik.stages.authenticator.models import Device
class AuthenticatorEndpointGDTCStage(ConfigurableStage, FriendlyNamedStage, Stage):
"""Setup Google Chrome Device-trust connection"""
"""Setup Google Chrome Device Trust connection"""
credentials = models.JSONField()

View File

@@ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor):
self.reader: Reader | None = None
self._last_mtime: float = 0.0
self.logger = get_logger()
self.open()
self.load()
def path(self) -> str | None:
"""Get the path to the MMDB file to load"""
raise NotImplementedError
def open(self):
def load(self):
"""Get GeoIP Reader, if configured, otherwise none"""
path = self.path()
if path == "" or not path:
@@ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor):
diff = self._last_mtime < mtime
if diff > 0:
self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
self.open()
self.load()
except OSError as exc:
self.logger.warning("Failed to check MMDB age", exc=exc)

View File

@@ -130,7 +130,7 @@ class SyncTasks:
def sync_objects(
self, object_type: str, page: int, provider_pk: int, override_dry_run=False, **filter
):
_object_type = path_to_class(object_type)
_object_type: type[Model] = path_to_class(object_type)
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
provider_pk=provider_pk,
@@ -156,7 +156,11 @@ class SyncTasks:
messages.append(
asdict(
LogEvent(
_("Syncing page {page} of groups".format(page=page)),
_(
"Syncing page {page} of {object_type}".format(
page=page, object_type=_object_type._meta.verbose_name_plural
)
),
log_level="info",
logger=f"{provider._meta.verbose_name}@{object_type}",
)

View File

@@ -1,11 +1,9 @@
"""Websocket tests"""
from dataclasses import asdict
from unittest.mock import patch
from channels.routing import URLRouter
from channels.testing import WebsocketCommunicator
from django.contrib.contenttypes.models import ContentType
from django.test import TransactionTestCase
from authentik import __version__
@@ -16,12 +14,6 @@ from authentik.providers.proxy.models import ProxyProvider
from authentik.root import websocket
def patched__get_ct_cached(app_label, codename):
"""Caches `ContentType` instances like its `QuerySet` does."""
return ContentType.objects.get(app_label=app_label, permission__codename=codename)
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestOutpostWS(TransactionTestCase):
"""Websocket tests"""

View File

@@ -102,6 +102,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
# Buffer sizes for large headers with JWTs
"nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
"nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
"nginx.ingress.kubernetes.io/proxy-busy-buffers-size": "32k",
# Enable TLS in traefik
"traefik.ingress.kubernetes.io/router.tls": "true",
}

View File

@@ -66,7 +66,10 @@ class RACClientConsumer(AsyncWebsocketConsumer):
def init_outpost_connection(self):
"""Initialize guac connection settings"""
self.token = (
ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"])
ConnectionToken.filter_not_expired(
token=self.scope["url_route"]["kwargs"]["token"],
session__session__session_key=self.scope["session"].session_key,
)
.select_related("endpoint", "provider", "session", "session__user")
.first()
)

View File

@@ -166,7 +166,6 @@ class ConnectionToken(ExpiringModel):
always_merger.merge(settings, default_settings)
always_merger.merge(settings, self.endpoint.provider.settings)
always_merger.merge(settings, self.endpoint.settings)
always_merger.merge(settings, self.settings)
def mapping_evaluator(mappings: QuerySet):
for mapping in mappings:
@@ -191,6 +190,7 @@ class ConnectionToken(ExpiringModel):
mapping_evaluator(
RACPropertyMapping.objects.filter(endpoint__in=[self.endpoint]).order_by("name")
)
always_merger.merge(settings, self.settings)
settings["drive-path"] = f"/tmp/connection/{self.token}" # nosec
settings["create-drive-path"] = "true"

View File

@@ -90,23 +90,6 @@ class TestModels(TransactionTestCase):
"resize-method": "display-update",
},
)
# Set settings in token
token.settings = {
"level": "token",
}
token.save()
self.assertEqual(
token.get_settings(),
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"level": "token",
"resize-method": "display-update",
},
)
# Set settings in property mapping (provider)
mapping = RACPropertyMapping.objects.create(
name=generate_id(),
@@ -151,3 +134,22 @@ class TestModels(TransactionTestCase):
"resize-method": "display-update",
},
)
# Set settings in token
token.settings = {
"level": "token",
}
token.save()
self.assertEqual(
token.get_settings(),
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"foo": "true",
"bar": "6",
"resize-method": "display-update",
"level": "token",
},
)

View File

@@ -87,3 +87,22 @@ class TestRACViews(APITestCase):
)
body = loads(flow_response.content)
self.assertEqual(body["component"], "ak-stage-access-denied")
def test_different_session(self):
"""Test request"""
self.client.force_login(self.user)
response = self.client.get(
reverse(
"authentik_providers_rac:start",
kwargs={"app": self.app.slug, "endpoint": str(self.endpoint.pk)},
)
)
self.assertEqual(response.status_code, 302)
flow_response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
body = loads(flow_response.content)
next_url = body["to"]
self.client.logout()
final_response = self.client.get(next_url)
self.assertEqual(final_response.url, reverse("authentik_core:if-user"))

View File

@@ -20,6 +20,9 @@ from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
PLAN_CONNECTION_SETTINGS = "connection_settings"
class RACStartView(PolicyAccessView):
@@ -65,7 +68,10 @@ class RACInterface(InterfaceView):
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
# Early sanity check to ensure token still exists
token = ConnectionToken.filter_not_expired(token=self.kwargs["token"]).first()
token = ConnectionToken.filter_not_expired(
token=self.kwargs["token"],
session__session__session_key=request.session.session_key,
).first()
if not token:
return redirect("authentik_core:if-user")
self.token = token
@@ -109,10 +115,15 @@ class RACFinalStage(RedirectStage):
return super().dispatch(request, *args, **kwargs)
def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
settings = self.executor.plan.context.get(PLAN_CONNECTION_SETTINGS)
if not settings:
settings = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).get(
PLAN_CONNECTION_SETTINGS
)
token = ConnectionToken.objects.create(
provider=self.provider,
endpoint=self.endpoint,
settings=self.executor.plan.context.get("connection_settings", {}),
settings=settings or {},
session=self.request.session["authenticatedsession"],
expires=now() + timedelta_from_string(self.provider.connection_expiry),
expiring=True,

View File

@@ -3,25 +3,46 @@
import os
from argparse import ArgumentParser
from unittest import TestCase
from unittest.mock import patch
import pytest
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.test.runner import DiscoverRunner
from structlog.stdlib import get_logger
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.lib.config import CONFIG
from authentik.lib.sentry import sentry_init
from authentik.root.signals import post_startup, pre_startup, startup
from tests.e2e.utils import get_docker_tag
# globally set maxDiff to none to show full assert error
TestCase.maxDiff = None
def get_docker_tag() -> str:
"""Get docker-tag based off of CI variables"""
env_pr_branch = "GITHUB_HEAD_REF"
default_branch = "GITHUB_REF"
branch_name = os.environ.get(default_branch, "main")
if os.environ.get(env_pr_branch, "") != "":
branch_name = os.environ[env_pr_branch]
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
return f"gh-{branch_name}"
def patched__get_ct_cached(app_label, codename):
"""Caches `ContentType` instances like its `QuerySet` does."""
return ContentType.objects.get(app_label=app_label, permission__codename=codename)
class PytestTestRunner(DiscoverRunner): # pragma: no cover
"""Runs pytest to discover and run tests."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.logger = get_logger().bind(runner="pytest")
self.args = []
if self.failfast:
@@ -48,6 +69,10 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
CONFIG.set("error_reporting.sample_rate", 0)
CONFIG.set("error_reporting.environment", "testing")
CONFIG.set("error_reporting.send_pii", True)
ASN_CONTEXT_PROCESSOR.load()
GEOIP_CONTEXT_PROCESSOR.load()
sentry_init()
pre_startup.send(sender=self, mode="test")
@@ -113,4 +138,10 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
f"path instead."
)
return pytest.main(self.args)
self.logger.info("Running tests", test_files=self.args)
with patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached):
try:
return pytest.main(self.args)
except Exception as e:
self.logger.error("Error running tests", error=str(e), test_files=self.args)
return 1

View File

@@ -71,37 +71,31 @@ def ldap_sync_single(source_pk: str):
return
# Delete all sync tasks from the cache
DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
task = chain(
# User and group sync can happen at once, they have no dependencies on each other
group(
ldap_sync_paginator(source, UserLDAPSynchronizer)
+ ldap_sync_paginator(source, GroupLDAPSynchronizer),
),
# Membership sync needs to run afterwards
group(
ldap_sync_paginator(source, MembershipLDAPSynchronizer),
),
# Finally, deletions. What we'd really like to do here is something like
# ```
# user_identifiers = <ldap query>
# User.objects.exclude(
# usersourceconnection__identifier__in=user_uniqueness_identifiers,
# ).delete()
# ```
# This runs into performance issues in large installations. So instead we spread the
# work out into three steps:
# 1. Get every object from the LDAP source.
# 2. Mark every object as "safe" in the database. This is quick, but any error could
# mean deleting users which should not be deleted, so we do it immediately, in
# large chunks, and only queue the deletion step afterwards.
# 3. Delete every unmarked item. This is slow, so we spread it over many tasks in
# small chunks.
group(
ldap_sync_paginator(source, UserLDAPForwardDeletion)
+ ldap_sync_paginator(source, GroupLDAPForwardDeletion),
),
# The order of these operations needs to be preserved as each depends on the previous one(s)
# 1. User and group sync can happen simultaneously
# 2. Membership sync needs to run afterwards
# 3. Finally, user and group deletions can happen simultaneously
user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator(
source, GroupLDAPSynchronizer
)
task()
membership_sync = ldap_sync_paginator(source, MembershipLDAPSynchronizer)
user_group_deletion = ldap_sync_paginator(
source, UserLDAPForwardDeletion
) + ldap_sync_paginator(source, GroupLDAPForwardDeletion)
# Celery is buggy with empty groups, so we are careful only to add non-empty groups.
# See https://github.com/celery/celery/issues/9772
task_groups = []
if user_group_sync:
task_groups.append(group(user_group_sync))
if membership_sync:
task_groups.append(group(membership_sync))
if user_group_deletion:
task_groups.append(group(user_group_deletion))
all_tasks = chain(task_groups)
all_tasks()
def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list:

View File

@@ -151,9 +151,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
webauthn_user_verification=UserVerification.PREFERRED,
)
stage.webauthn_allowed_device_types.set(
WebAuthnDeviceType.objects.filter(
description="Android Authenticator with SafetyNet Attestation"
)
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series")
)
session = self.client.session
plan = FlowPlan(flow_pk=flow.pk.hex)
@@ -339,9 +337,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
device_classes=[DeviceClasses.WEBAUTHN],
)
stage.webauthn_allowed_device_types.set(
WebAuthnDeviceType.objects.filter(
description="Android Authenticator with SafetyNet Attestation"
)
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series")
)
session = self.client.session
plan = FlowPlan(flow_pk=flow.pk.hex)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -141,9 +141,7 @@ class TestAuthenticatorWebAuthnStage(FlowTestCase):
"""Test registration with restricted devices (fail)"""
webauthn_mds_import.delay(force=True).get()
self.stage.device_type_restrictions.set(
WebAuthnDeviceType.objects.filter(
description="Android Authenticator with SafetyNet Attestation"
)
WebAuthnDeviceType.objects.filter(description="YubiKey 5 Series")
)
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

View File

@@ -100,9 +100,11 @@ def send_mail(
# Because we use the Message-ID as UID for the task, manually assign it
message_object.extra_headers["Message-ID"] = message_id
# Add the logo (we can't add it in the previous message since MIMEImage
# can't be converted to json)
message_object.attach(logo_data())
# Add the logo if it is used in the email body (we can't add it in the
# previous message since MIMEImage can't be converted to json)
body = get_email_body(message_object)
if "cid:logo" in body:
message_object.attach(logo_data())
if (
message_object.to

View File

@@ -96,7 +96,7 @@
<table width="100%" style="background-color: #FFFFFF; border-spacing: 0; margin-top: 15px;">
<tr height="80">
<td align="center" style="padding: 20px 0;">
<img src="{% block logo_url %}cid:logo.png{% endblock %}" border="0=" alt="authentik logo" class="flexibleImage logo">
<img src="{% block logo_url %}cid:logo{% endblock %}" border="0=" alt="authentik logo" class="flexibleImage logo">
</td>
</tr>
{% block content %}

View File

@@ -19,7 +19,8 @@ def logo_data() -> MIMEImage:
path = Path("web/dist/assets/icons/icon_left_brand.png")
with open(path, "rb") as _logo_file:
logo = MIMEImage(_logo_file.read())
logo.add_header("Content-ID", "logo.png")
logo.add_header("Content-ID", "<logo>")
logo.add_header("Content-Disposition", "inline", filename="logo.png")
return logo

View File

@@ -89,6 +89,29 @@ class TestPasswordStage(FlowTestCase):
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
def test_valid_password_inactive(self):
"""Test with a valid pending user and valid password"""
self.user.is_active = False
self.user.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
# Form data
{"password": self.user.username},
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={"password": [{"string": "Invalid password", "code": "invalid"}]},
)
def test_invalid_password(self):
"""Test with a valid pending user and invalid password"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

View File

@@ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware):
SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
)
if configured_binding_net != NetworkBinding.NO_BINDING:
self.recheck_session_net(configured_binding_net, last_ip, new_ip)
BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip)
if configured_binding_geo != GeoIPBinding.NO_BINDING:
self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
BoundSessionMiddleware.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
# If we got to this point without any error being raised, we need to
# update the last saved IP to the current one
if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session:
@@ -111,7 +111,8 @@ class BoundSessionMiddleware(SessionMiddleware):
# (== basically requires the user to be logged in)
request.session[request.session.model.Keys.LAST_IP] = new_ip
def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
@staticmethod
def recheck_session_net(binding: NetworkBinding, last_ip: str, new_ip: str):
"""Check network/ASN binding"""
last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
@@ -158,7 +159,8 @@ class BoundSessionMiddleware(SessionMiddleware):
new_ip,
)
def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
@staticmethod
def recheck_session_geo(binding: GeoIPBinding, last_ip: str, new_ip: str):
"""Check GeoIP binding"""
last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
@@ -179,8 +181,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.continent != new_geo.continent:
raise SessionBindingBroken(
"geoip.continent",
last_geo.continent,
new_geo.continent,
last_geo.continent.to_dict(),
new_geo.continent.to_dict(),
last_ip,
new_ip,
)
@@ -192,8 +194,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.country != new_geo.country:
raise SessionBindingBroken(
"geoip.country",
last_geo.country,
new_geo.country,
last_geo.country.to_dict(),
new_geo.country.to_dict(),
last_ip,
new_ip,
)
@@ -202,8 +204,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.city != new_geo.city:
raise SessionBindingBroken(
"geoip.city",
last_geo.city,
new_geo.city,
last_geo.city.to_dict(),
new_geo.city.to_dict(),
last_ip,
new_ip,
)

View File

@@ -91,6 +91,7 @@ class UserLoginStageView(ChallengeStageView):
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
if not user.is_active:
self.logger.warning("User is not active, login will not work.")
return self.executor.stage_invalid()
delta = self.set_session_duration(remember)
self.set_session_ip()
# the `user_logged_in` signal will update the user to write the `last_login` field

View File

@@ -3,9 +3,11 @@
from time import sleep
from unittest.mock import patch
from django.http import HttpRequest
from django.urls import reverse
from django.utils.timezone import now
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import AuthenticatedSession, Session
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.markers import StageMarker
@@ -17,7 +19,12 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.utils.time import timedelta_from_string
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.user_login.models import UserLoginStage
from authentik.stages.user_login.middleware import (
BoundSessionMiddleware,
SessionBindingBroken,
logout_extra,
)
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage
class TestUserLoginStage(FlowTestCase):
@@ -174,6 +181,7 @@ class TestUserLoginStage(FlowTestCase):
component="ak-stage-access-denied",
)
@apply_blueprint("default/flow-default-user-settings-flow.yaml")
def test_inactive_account(self):
"""Test with a valid pending user and backend"""
self.user.is_active = False
@@ -187,8 +195,74 @@ class TestUserLoginStage(FlowTestCase):
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
self.assertStageResponse(
response, self.flow, component="ak-stage-access-denied", error_message="Unknown error"
)
# Check that API requests get rejected
response = self.client.get(reverse("authentik_api:application-list"))
self.assertEqual(response.status_code, 403)
# Check that flow requests requiring a user also get rejected
response = self.client.get(
reverse(
"authentik_api:flow-executor",
kwargs={"flow_slug": "default-user-settings-flow"},
)
)
self.assertStageResponse(
response,
self.flow,
component="ak-stage-access-denied",
error_message="Flow does not apply to current user.",
)
def test_binding_net_break_log(self):
"""Test logout_extra with exception"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json
for args, expect in [
[[NetworkBinding.BIND_ASN, "8.8.8.8", "8.8.8.8"], ["network.missing"]],
[[NetworkBinding.BIND_ASN, "1.0.0.1", "1.128.0.1"], ["network.asn"]],
[
[NetworkBinding.BIND_ASN_NETWORK, "12.81.96.1", "12.81.128.1"],
["network.asn_network"],
],
[[NetworkBinding.BIND_ASN_NETWORK_IP, "1.0.0.1", "1.0.0.2"], ["network.ip"]],
]:
with self.subTest(args[0]):
with self.assertRaises(SessionBindingBroken) as cm:
BoundSessionMiddleware.recheck_session_net(*args)
self.assertEqual(cm.exception.reason, expect[0])
# Ensure the request can be logged without throwing errors
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
request.user = self.user
logout_extra(request, cm.exception)
def test_binding_geo_break_log(self):
"""Test logout_extra with exception"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
for args, expect in [
[[GeoIPBinding.BIND_CONTINENT, "8.8.8.8", "8.8.8.8"], ["geoip.missing"]],
[[GeoIPBinding.BIND_CONTINENT, "2.125.160.216", "67.43.156.1"], ["geoip.continent"]],
[
[GeoIPBinding.BIND_CONTINENT_COUNTRY, "81.2.69.142", "89.160.20.112"],
["geoip.country"],
],
[
[GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY, "2.125.160.216", "81.2.69.142"],
["geoip.city"],
],
]:
with self.subTest(args[0]):
with self.assertRaises(SessionBindingBroken) as cm:
BoundSessionMiddleware.recheck_session_geo(*args)
self.assertEqual(cm.exception.reason, expect[0])
# Ensure the request can be logged without throwing errors
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
request.user = self.user
logout_extra(request, cm.exception)

View File

@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2025.4.1 Blueprint schema",
"title": "authentik 2025.6.4 Blueprint schema",
"required": [
"version",
"entries"

View File

@@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.4}
restart: unless-stopped
command: server
environment:
@@ -55,7 +55,7 @@ services:
redis:
condition: service_healthy
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.4}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@@ -27,7 +27,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025041.2
goauthentik.io/api/v3 v3.2025041.4
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.14.0

4
go.sum
View File

@@ -290,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025041.2 h1:vFYYnhcDcxL95RczZwhzt3i4LptFXMvIRN+vgf8sQYg=
goauthentik.io/api/v3 v3.2025041.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025041.4 h1:cGqzWYnUHrWDoaXWDpIL/kWnX9sFrIhkYDye0P0OEAo=
goauthentik.io/api/v3 v3.2025041.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@@ -33,4 +33,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2025.4.1"
const VERSION = "2025.6.4"

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"crypto/tls"
"encoding/gob"
"encoding/hex"
"fmt"
"html/template"
"net/http"
@@ -118,8 +119,8 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
mux := mux.NewRouter()
// Save cookie name, based on hashed client ID
h := sha256.New()
bs := string(h.Sum([]byte(*p.ClientId)))
hs := sha256.Sum256([]byte(*p.ClientId))
bs := hex.EncodeToString(hs[:])
sessionName := fmt.Sprintf("authentik_proxy_%s", bs[:8])
// When HOST_BROWSER is set, use that as Host header for token requests to make the issuer match

View File

@@ -3,6 +3,7 @@ package application
type ProxyClaims struct {
UserAttributes map[string]interface{} `json:"user_attributes"`
BackendOverride string `json:"backend_override"`
HostHeader string `json:"host_header"`
IsSuperuser bool `json:"is_superuser"`
}

View File

@@ -74,13 +74,18 @@ func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
r.URL.Scheme = ou.Scheme
r.URL.Host = ou.Host
claims := a.getClaimsFromSession(r)
if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" {
u, err := url.Parse(claims.Proxy.BackendOverride)
if err != nil {
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
} else {
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
if claims != nil && claims.Proxy != nil {
if claims.Proxy.BackendOverride != "" {
u, err := url.Parse(claims.Proxy.BackendOverride)
if err != nil {
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
} else {
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
}
}
if claims.Proxy.HostHeader != "" {
r.Host = claims.Proxy.HostHeader
}
}
a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url")

View File

@@ -2,6 +2,7 @@ package radius
import (
"encoding/base64"
"errors"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
@@ -11,9 +12,7 @@ import (
"layeh.com/radius/rfc2865"
)
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
username := rfc2865.UserName_GetString(r.Packet)
func (rs *RadiusServer) Handle_AccessRequest_PAP_Auth(r *RadiusRequest, username, password string) (*radius.Packet, error) {
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
"username": username,
"client": r.RemoteAddr(),
@@ -23,67 +22,64 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
fe.Params.Add("goauthentik.io/outpost/radius", "true")
fe.Answers[flow.StageIdentification] = username
fe.SetSecrets(rfc2865.UserPassword_GetString(r.Packet), r.pi.MFASupport)
fe.SetSecrets(password, r.pi.MFASupport)
passed, err := fe.Execute()
if err != nil {
r.Log().WithField("username", username).WithError(err).Warning("failed to execute flow")
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "flow_error",
"app": r.pi.appSlug,
}).Inc()
_ = w.Write(r.Response(radius.CodeAccessReject))
return
return nil, errors.New("flow_error")
}
if !passed {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "invalid_credentials",
"app": r.pi.appSlug,
}).Inc()
_ = w.Write(r.Response(radius.CodeAccessReject))
return
return nil, errors.New("invalid_credentials")
}
access, _, err := fe.ApiClient().OutpostsApi.OutpostsRadiusAccessCheck(
r.Context(), r.pi.providerId,
).AppSlug(r.pi.appSlug).Execute()
if err != nil {
r.Log().WithField("username", username).WithError(err).Warning("failed to check access")
_ = w.Write(r.Response(radius.CodeAccessReject))
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "access_check_fail",
"app": r.pi.appSlug,
}).Inc()
return
return nil, errors.New("access_check_fail")
}
if !access.Access.Passing {
r.Log().WithField("username", username).Info("Access denied for user")
_ = w.Write(r.Response(radius.CodeAccessReject))
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": "access_denied",
"app": r.pi.appSlug,
}).Inc()
return
return nil, errors.New("access_denied")
}
res := r.Response(radius.CodeAccessAccept)
defer func() { _ = w.Write(res) }()
if !access.HasAttributes() {
r.Log().Debug("No attributes")
return
return res, nil
}
rawData, err := base64.StdEncoding.DecodeString(access.GetAttributes())
if err != nil {
r.Log().WithError(err).Warning("failed to decode attributes from core")
return
return nil, errors.New("attribute_decode_failed")
}
p, err := radius.Parse(rawData, r.pi.SharedSecret)
if err != nil {
r.Log().WithError(err).Warning("failed to parse attributes from core")
return nil, errors.New("attribute_parse_failed")
}
for _, attr := range p.Attributes {
res.Add(attr.Type, attr.Attribute)
}
return res, nil
}
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
username := rfc2865.UserName_GetString(r.Packet)
password := rfc2865.UserPassword_GetString(r.Packet)
res, err := rs.Handle_AccessRequest_PAP_Auth(r, username, password)
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": rs.ac.Outpost.Name,
"reason": err.Error(),
"app": r.pi.appSlug,
}).Inc()
_ = w.Write(r.Reject())
return
}
err = r.setMessageAuthenticator(res)
if err != nil {
rs.log.WithError(err).Warning("failed to set message authenticator")
}
_ = w.Write(res)
}

View File

@@ -1,7 +1,12 @@
package radius
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha512"
"encoding/hex"
"errors"
"time"
"github.com/getsentry/sentry-go"
@@ -12,6 +17,11 @@ import (
"goauthentik.io/internal/outpost/radius/metrics"
"goauthentik.io/internal/utils"
"layeh.com/radius"
"layeh.com/radius/rfc2869"
)
var (
ErrInvalidMessageAuthenticator = errors.New("invalid message authenticator")
)
type RadiusRequest struct {
@@ -34,6 +44,41 @@ func (r *RadiusRequest) ID() string {
return r.id
}
func (r *RadiusRequest) validateMessageAuthenticator() error {
mauth := rfc2869.MessageAuthenticator_Get(r.Packet)
hash := hmac.New(md5.New, r.Secret)
encode, err := r.MarshalBinary()
if err != nil {
return err
}
hash.Write(encode)
if bytes.Equal(mauth, hash.Sum(nil)) {
return ErrInvalidMessageAuthenticator
}
return nil
}
func (r *RadiusRequest) setMessageAuthenticator(rp *radius.Packet) error {
_ = rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16))
hash := hmac.New(md5.New, rp.Secret)
encode, err := rp.MarshalBinary()
if err != nil {
return err
}
hash.Write(encode)
_ = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil))
return nil
}
func (r *RadiusRequest) Reject() *radius.Packet {
res := r.Response(radius.CodeAccessReject)
err := r.setMessageAuthenticator(res)
if err != nil {
r.log.WithError(err).Warning("failed to set message authenticator")
}
return res
}
func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
span := sentry.StartSpan(r.Context(), "authentik.providers.radius.connect",
sentry.WithTransactionName("authentik.providers.radius.connect"))
@@ -58,6 +103,11 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
rl.Info("Radius Request")
if err := nr.validateMessageAuthenticator(); err != nil {
rl.WithError(err).Warning("Invalid message authenticator")
return
}
// Lookup provider by shared secret
var pi *ProviderInstance
for _, p := range rs.providers {
@@ -68,8 +118,10 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
}
}
if pi == nil {
nr.Log().WithField("hashed_secret", string(sha512.New().Sum(r.Secret))).Warning("No provider found")
_ = w.Write(r.Response(radius.CodeAccessReject))
hs := sha512.Sum512([]byte(r.Secret))
bs := hex.EncodeToString(hs[:])
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
_ = w.Write(nr.Reject())
return
}
nr.pi = pi

View File

@@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2025.4.1
Default: 2025.6.4
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
"POT-Creation-Date: 2025-06-02 00:12+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -2226,6 +2226,10 @@ msgstr ""
msgid "Consider Objects matching this filter to be Users."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Attribute which matches the value of `group_membership_field`."
msgstr ""
#: authentik/sources/ldap/models.py
msgid "Field which contains members of a group."
msgstr ""
@@ -3493,10 +3497,6 @@ msgstr ""
msgid "No Pending user to login."
msgstr ""
#: authentik/stages/user_login/stage.py
msgid "Successfully logged in!"
msgstr ""
#: authentik/stages/user_logout/models.py
msgid "User Logout Stage"
msgstr ""

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/authentik",
"version": "2025.4.1",
"version": "2025.6.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2025.4.1",
"version": "2025.6.4",
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.3.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/authentik",
"version": "2025.4.1",
"version": "2025.6.4",
"private": true,
"type": "module",
"devDependencies": {

View File

@@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2025.4.1"
version = "2025.6.4"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*"
@@ -13,7 +13,7 @@ dependencies = [
"dacite==1.9.2",
"deepmerge==2.0",
"defusedxml==0.7.1",
"django==5.1.9",
"django==5.1.11",
"django-countries==7.6.1",
"django-cte==1.3.3",
"django-filter==25.1",
@@ -61,7 +61,7 @@ dependencies = [
"setproctitle==1.3.6",
"structlog==25.3.0",
"swagger-spec-validator==3.0.4",
"tenant-schemas-celery==4.0.1",
"tenant-schemas-celery==3.0.0",
"twilio==9.6.1",
"ua-parser==1.0.1",
"unidecode==1.4.0",

View File

@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2025.4.1
version: 2025.6.4
description: Making authentication simple.
contact:
email: hello@goauthentik.io

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -2,7 +2,6 @@
from dataclasses import asdict
from time import sleep
from unittest.mock import patch
from guardian.shortcuts import assign_perm
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
@@ -16,12 +15,10 @@ from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
from authentik.outposts.tests.test_ws import patched__get_ct_cached
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider
from tests.e2e.utils import SeleniumTestCase, retry
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestProviderLDAP(SeleniumTestCase):
"""LDAP and Outpost e2e tests"""

View File

@@ -6,7 +6,6 @@ from json import loads
from sys import platform
from time import sleep
from unittest.case import skip, skipUnless
from unittest.mock import patch
from channels.testing import ChannelsLiveServerTestCase
from jwt import decode
@@ -18,12 +17,10 @@ from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
from authentik.outposts.tasks import outpost_connection_discovery
from authentik.outposts.tests.test_ws import patched__get_ct_cached
from authentik.providers.proxy.models import ProxyProvider
from tests.e2e.utils import SeleniumTestCase, retry
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestProviderProxy(SeleniumTestCase):
"""Proxy and Outpost e2e tests"""

View File

@@ -4,7 +4,6 @@ from json import loads
from pathlib import Path
from time import sleep
from unittest import skip
from unittest.mock import patch
from selenium.webdriver.common.by import By
@@ -13,12 +12,10 @@ from authentik.core.models import Application
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.outposts.models import Outpost, OutpostType
from authentik.outposts.tests.test_ws import patched__get_ct_cached
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
from tests.e2e.utils import SeleniumTestCase, retry
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestProviderProxyForward(SeleniumTestCase):
"""Proxy and Outpost e2e tests"""

View File

@@ -2,9 +2,8 @@
from dataclasses import asdict
from time import sleep
from unittest.mock import patch
from pyrad.client import Client
from pyrad.client import Client, Timeout
from pyrad.dictionary import Dictionary
from pyrad.packet import AccessAccept, AccessReject, AccessRequest
@@ -13,12 +12,10 @@ from authentik.core.models import Application, User
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id, generate_key
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
from authentik.outposts.tests.test_ws import patched__get_ct_cached
from authentik.providers.radius.models import RadiusProvider
from tests.e2e.utils import SeleniumTestCase, retry
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestProviderRadius(SeleniumTestCase):
"""Radius Outpost e2e tests"""
@@ -30,7 +27,7 @@ class TestProviderRadius(SeleniumTestCase):
"""Start radius container based on outpost created"""
self.run_container(
image=self.get_container_image("ghcr.io/goauthentik/dev-radius"),
ports={"1812/udp": "1812/udp"},
ports={"1812/udp": 1812},
environment={
"AUTHENTIK_TOKEN": outpost.token.key,
},
@@ -66,7 +63,7 @@ class TestProviderRadius(SeleniumTestCase):
sleep(5)
return outpost
@retry()
@retry(exceptions=[Timeout])
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
@@ -88,7 +85,7 @@ class TestProviderRadius(SeleniumTestCase):
reply = srv.SendPacket(req)
self.assertEqual(reply.code, AccessAccept)
@retry()
@retry(exceptions=[Timeout])
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",

View File

@@ -1,7 +1,6 @@
"""authentik e2e testing utilities"""
import json
import os
import socket
from collections.abc import Callable
from functools import lru_cache, wraps
@@ -37,22 +36,12 @@ from authentik.core.api.users import UserSerializer
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.root.test_runner import get_docker_tag
IS_CI = "CI" in environ
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
def get_docker_tag() -> str:
"""Get docker-tag based off of CI variables"""
env_pr_branch = "GITHUB_HEAD_REF"
default_branch = "GITHUB_REF"
branch_name = os.environ.get(default_branch, "main")
if os.environ.get(env_pr_branch, "") != "":
branch_name = os.environ[env_pr_branch]
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
return f"gh-{branch_name}"
def get_local_ip() -> str:
"""Get the local machine's IP"""
hostname = socket.gethostname()

71
uv.lock generated
View File

@@ -164,7 +164,7 @@ wheels = [
[[package]]
name = "authentik"
version = "2025.4.1"
version = "2025.6.4"
source = { editable = "." }
dependencies = [
{ name = "argon2-cffi" },
@@ -273,7 +273,7 @@ requires-dist = [
{ name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" },
{ name = "django", specifier = "==5.1.9" },
{ name = "django", specifier = "==5.1.11" },
{ name = "django-countries", specifier = "==7.6.1" },
{ name = "django-cte", specifier = "==1.3.3" },
{ name = "django-filter", specifier = "==25.1" },
@@ -321,7 +321,7 @@ requires-dist = [
{ name = "setproctitle", specifier = "==1.3.6" },
{ name = "structlog", specifier = "==25.3.0" },
{ name = "swagger-spec-validator", specifier = "==3.0.4" },
{ name = "tenant-schemas-celery", specifier = "==4.0.1" },
{ name = "tenant-schemas-celery", specifier = "==3.0.0" },
{ name = "twilio", specifier = "==9.6.1" },
{ name = "ua-parser", specifier = "==1.0.1" },
{ name = "unidecode", specifier = "==1.4.0" },
@@ -979,16 +979,16 @@ wheels = [
[[package]]
name = "django"
version = "5.1.9"
version = "5.1.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/08/2e6f05494b3fc0a3c53736846034f882b82ee6351791a7815bbb45715d79/django-5.1.9.tar.gz", hash = "sha256:565881bdd0eb67da36442e9ac788bda90275386b549070d70aee86327781a4fc", size = 10710887, upload-time = "2025-05-07T14:06:45.257Z" }
sdist = { url = "https://files.pythonhosted.org/packages/83/80/bf0f9b0aa434fca2b46fc6a31c39b08ea714b87a0a72a16566f053fb05a8/django-5.1.11.tar.gz", hash = "sha256:3bcdbd40e4d4623b5e04f59c28834323f3086df583058e65ebce99f9982385ce", size = 10734926, upload-time = "2025-06-10T10:12:48.229Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/d1/d8b6b8250b84380d5a123e099ad3298a49407d81598faa13b43a2c6d96d7/django-5.1.9-py3-none-any.whl", hash = "sha256:2fd1d4a0a66a5ba702699eb692e75b0d828b73cc2f4e1fc4b6a854a918967411", size = 8277363, upload-time = "2025-05-07T14:06:37.426Z" },
{ url = "https://files.pythonhosted.org/packages/59/91/2972ce330c6c0bd5b3200d4c2ad5cbf47eecff5243220c5a56444d3267a0/django-5.1.11-py3-none-any.whl", hash = "sha256:e48091f364007068728aca938e7450fbfe3f2217079bfd2b8af45122585acf64", size = 8277453, upload-time = "2025-06-10T10:12:42.236Z" },
]
[[package]]
@@ -2383,16 +2383,16 @@ wheels = [
[[package]]
name = "protobuf"
version = "6.30.2"
version = "6.31.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/8c/cf2ac658216eebe49eaedf1e06bc06cbf6a143469236294a1171a51357c3/protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048", size = 429315, upload-time = "2025-03-26T19:12:57.394Z" }
sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/85/cd53abe6a6cbf2e0029243d6ae5fb4335da2996f6c177bb2ce685068e43d/protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103", size = 419148, upload-time = "2025-03-26T19:12:41.359Z" },
{ url = "https://files.pythonhosted.org/packages/97/e9/7b9f1b259d509aef2b833c29a1f3c39185e2bf21c9c1be1cd11c22cb2149/protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9", size = 431003, upload-time = "2025-03-26T19:12:44.156Z" },
{ url = "https://files.pythonhosted.org/packages/8e/66/7f3b121f59097c93267e7f497f10e52ced7161b38295137a12a266b6c149/protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b", size = 417579, upload-time = "2025-03-26T19:12:45.447Z" },
{ url = "https://files.pythonhosted.org/packages/d0/89/bbb1bff09600e662ad5b384420ad92de61cab2ed0f12ace1fd081fd4c295/protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815", size = 317319, upload-time = "2025-03-26T19:12:46.999Z" },
{ url = "https://files.pythonhosted.org/packages/28/50/1925de813499546bc8ab3ae857e3ec84efe7d2f19b34529d0c7c3d02d11d/protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d", size = 316212, upload-time = "2025-03-26T19:12:48.458Z" },
{ url = "https://files.pythonhosted.org/packages/e5/a1/93c2acf4ade3c5b557d02d500b06798f4ed2c176fa03e3c34973ca92df7f/protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51", size = 167062, upload-time = "2025-03-26T19:12:55.892Z" },
{ url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" },
{ url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" },
{ url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" },
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
]
[[package]]
@@ -2776,7 +2776,7 @@ wheels = [
[[package]]
name = "requests"
version = "2.32.3"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -2784,9 +2784,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
@@ -3100,32 +3100,33 @@ wheels = [
[[package]]
name = "tenant-schemas-celery"
version = "4.0.1"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "celery" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/f8/cf055bf171b5d83d6fe96f1840fba90d3d274be2b5c35cd21b873302b128/tenant_schemas_celery-4.0.1.tar.gz", hash = "sha256:8b8f055fcd82aa53274c09faf88653a935241518d93b86ab2d43a3df3b70c7f8", size = 18870, upload-time = "2025-04-22T18:23:51.061Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/fe/cfe19eb7cc3ad8e39d7df7b7c44414bf665b6ac6660c998eb498f89d16c6/tenant_schemas_celery-3.0.0.tar.gz", hash = "sha256:6be3ae1a5826f262f0f3dd343c6a85a34a1c59b89e04ae37de018f36562fed55", size = 15954, upload-time = "2024-05-19T11:16:41.837Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/a8/fd663c461550d6fedfb24e987acc1557ae5b6615ca08fc6c70dbaaa88aa5/tenant_schemas_celery-4.0.1-py3-none-any.whl", hash = "sha256:d06a3ff6956db3a95168ce2051b7bff2765f9ce0d070e14df92f07a2b60ae0a0", size = 21364, upload-time = "2025-04-22T18:23:49.899Z" },
{ url = "https://files.pythonhosted.org/packages/db/2c/376e1e641ad08b374c75d896468a7be2e6906ce3621fd0c9f9dc09ff1963/tenant_schemas_celery-3.0.0-py3-none-any.whl", hash = "sha256:ca0f69e78ef698eb4813468231df5a0ab6a660c08e657b65f5ac92e16887eec8", size = 18108, upload-time = "2024-05-19T11:16:39.92Z" },
]
[[package]]
name = "tornado"
version = "6.4.2"
version = "6.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135, upload-time = "2024-11-22T03:06:38.036Z" }
sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299, upload-time = "2024-11-22T03:06:20.162Z" },
{ url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253, upload-time = "2024-11-22T03:06:22.39Z" },
{ url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602, upload-time = "2024-11-22T03:06:24.214Z" },
{ url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972, upload-time = "2024-11-22T03:06:25.559Z" },
{ url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173, upload-time = "2024-11-22T03:06:27.584Z" },
{ url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892, upload-time = "2024-11-22T03:06:28.933Z" },
{ url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334, upload-time = "2024-11-22T03:06:30.428Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261, upload-time = "2024-11-22T03:06:32.458Z" },
{ url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463, upload-time = "2024-11-22T03:06:34.71Z" },
{ url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907, upload-time = "2024-11-22T03:06:36.71Z" },
{ url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" },
{ url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" },
{ url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" },
{ url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" },
{ url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" },
{ url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" },
{ url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" },
{ url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" },
{ url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" },
{ url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" },
{ url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" },
]
[[package]]
@@ -3287,11 +3288,11 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.4.0"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[package.optional-dependencies]

View File

@@ -85,8 +85,8 @@ export class AdminOverviewPage extends AdminOverviewBase {
render(): TemplateResult {
const username = this.user?.user.name || this.user?.user.username;
return html` <ak-page-header
header=${msg(str`Welcome, ${username || ""}.`)}
return html`<ak-page-header
header=${this.user ? msg(str`Welcome, ${username || ""}.`) : msg("Welcome.")}
description=${msg("General system status")}
?hasIcon=${false}
>

View File

@@ -361,7 +361,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Addition User DN")}
label=${msg("Additional User DN")}
name="additionalUserDn"
>
<input
@@ -374,7 +374,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Addition Group DN")}
label=${msg("Additional Group DN")}
name="additionalGroupDn"
>
<input

View File

@@ -73,6 +73,12 @@ html > form > input {
/* #endregion */
/* #region Form */
.pf-c-form {
--pf-c-form__group--m-action--MarginTop: var(--pf-global--spacer--form-element);
}
/* #region Icons */
.pf-icon {

View File

@@ -201,6 +201,10 @@ select[multiple] option:checked {
--pf-c-input-group--BackgroundColor: transparent;
}
select.pf-c-form-control {
--pf-c-form-control__select--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%23fafafa' d='M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z'/%3E%3C/svg%3E");
}
.pf-c-form-control {
--pf-c-form-control--BorderTopColor: transparent !important;
--pf-c-form-control--BorderRightColor: transparent !important;

View File

@@ -374,7 +374,7 @@ ${JSON.stringify(value.new_value, null, 4)}</pre
renderEmailSent() {
let body = this.event.context.body as string;
body = body.replace("cid:logo.png", "/static/dist/assets/icons/icon_left_brand.png");
body = body.replace("cid:logo", "/static/dist/assets/icons/icon_left_brand.png");
return html`<div class="pf-c-card__title">${msg("Email info:")}</div>
<div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div>
<ak-expand>

View File

@@ -147,7 +147,7 @@ export class AKPageNavbar
}
.accent-icon {
height: 1em;
height: 1.2em;
width: 1em;
@media (max-width: 768px) {
@@ -157,6 +157,7 @@ export class AKPageNavbar
}
&.page-description {
padding-top: 0.3em;
grid-area: description;
margin-block-end: var(--pf-global--spacer--md);

View File

@@ -123,6 +123,9 @@ export class AKElement extends LitElement {
applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet);
this.activeTheme = UiThemeEnum.Dark;
} else if (this.preferredColorScheme === "light") {
applyUITheme(nextStyleRoot, UiThemeEnum.Light, this.#customCSSStyleSheet);
this.activeTheme = UiThemeEnum.Light;
} else if (this.preferredColorScheme === "auto") {
createUIThemeEffect(
(nextUITheme) => {

View File

@@ -32,8 +32,8 @@ import {
} from "./types.js";
function localeComparator(a: DualSelectPair, b: DualSelectPair) {
const aSortBy = a[2];
const bSortBy = b[2];
const aSortBy = String(a[2] || a[0]);
const bSortBy = String(b[2] || b[0]);
return aSortBy.localeCompare(bSortBy);
}

View File

@@ -34,7 +34,7 @@ export type DualSelectPair<T = unknown> = [
/**
* A string to sort by. If not provided, the key will be used.
*/
sortBy: string,
sortBy?: string,
/**
* A local mapping of the key to the object. This is used by some specific apps.
*

View File

@@ -17,7 +17,7 @@ import "@goauthentik/elements/table/TableSearch";
import { SlottedTemplateResult } from "@goauthentik/elements/types";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -107,6 +107,9 @@ export abstract class Table<T> extends AKElement implements TableLike {
private isLoading = false;
#pageParam = `${this.tagName.toLowerCase()}-page`;
#searchParam = `${this.tagName.toLowerCase()}-search`;
searchEnabled(): boolean {
return false;
}
@@ -123,7 +126,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
data?: PaginatedResponse<T>;
@property({ type: Number })
page = getURLParam("tablePage", 1);
page = getURLParam(this.#pageParam, 1);
/**
* Set if your `selectedElements` use of the selection box is to enable bulk-delete,
@@ -209,7 +212,7 @@ export abstract class Table<T> extends AKElement implements TableLike {
await this.fetch();
});
if (this.searchEnabled()) {
this.search = getURLParam("search", "");
this.search = getURLParam(this.#searchParam, "");
}
}
@@ -462,12 +465,23 @@ export abstract class Table<T> extends AKElement implements TableLike {
return nothing;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
if (changedProperties.has("page")) {
updateURLParams({
[this.#pageParam]: changedProperties.get("page"),
});
}
if (changedProperties.has("search")) {
updateURLParams({
[this.#searchParam]: changedProperties.get("search"),
});
}
}
renderSearch(): TemplateResult {
const runSearch = (value: string) => {
this.search = value;
updateURLParams({
search: value,
});
this.page = 1;
this.fetch();
};
@@ -548,7 +562,6 @@ export abstract class Table<T> extends AKElement implements TableLike {
/* A simple pagination display, shown at both the top and bottom of the page. */
renderTablePagination(): TemplateResult {
const handler = (page: number) => {
updateURLParams({ tablePage: page });
this.page = page;
this.fetch();
};

View File

@@ -3,7 +3,7 @@ import { updateURLParams } from "#elements/router/RouteMatch";
import { Table } from "#elements/table/Table";
import { msg } from "@lit/localize";
import { CSSResult } from "lit";
import { CSSResult, nothing } from "lit";
import { TemplateResult, html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -44,7 +44,7 @@ export abstract class TablePage<T> extends Table<T> {
? inner
: html`<ak-empty-state icon=${this.pageIcon()} header="${msg("No objects found.")}">
<div slot="body">
${this.searchEnabled() ? this.renderEmptyClearSearch() : html``}
${this.searchEnabled() ? this.renderEmptyClearSearch() : nothing}
</div>
<div slot="primary">${this.renderObjectCreate()}</div>
</ak-empty-state>`}
@@ -60,9 +60,7 @@ export abstract class TablePage<T> extends Table<T> {
this.search = "";
this.requestUpdate();
this.fetch();
updateURLParams({
search: "",
});
this.page = 1;
}}
class="pf-c-button pf-m-link"
>

View File

@@ -1,55 +1,31 @@
/**
* @file IFrame Utilities
*/
import { renderStaticHTMLUnsafe } from "#common/purify";
interface IFrameLoadResult {
contentWindow: Window;
contentDocument: Document;
}
import { MaybeCompiledTemplateResult } from "lit";
export function pluckIFrameContent(iframe: HTMLIFrameElement) {
const contentWindow = iframe.contentWindow;
const contentDocument = iframe.contentDocument;
if (!contentWindow) {
throw new Error("Iframe contentWindow is not accessible");
}
if (!contentDocument) {
throw new Error("Iframe contentDocument is not accessible");
}
return {
contentWindow,
contentDocument,
};
}
export function resolveIFrameContent(iframe: HTMLIFrameElement): Promise<IFrameLoadResult> {
if (iframe.contentDocument?.readyState === "complete") {
return Promise.resolve(pluckIFrameContent(iframe));
}
return new Promise((resolve) => {
iframe.addEventListener("load", () => resolve(pluckIFrameContent(iframe)), { once: true });
});
export interface CreateHTMLObjectInit {
body: string | MaybeCompiledTemplateResult;
head?: string | MaybeCompiledTemplateResult;
}
/**
* Creates a minimal HTML wrapper for an iframe.
* Render untrusted HTML to a string without escaping it.
*
* @deprecated Use the `contentDocument.body` directly instead.
* @returns {string} The rendered HTML string.
*/
export function createIFrameHTMLWrapper(bodyContent: string): string {
const html = String.raw;
export function createDocumentTemplate(init: CreateHTMLObjectInit): string {
const body = renderStaticHTMLUnsafe(init.body);
const head = init.head ? renderStaticHTMLUnsafe(init.head) : "";
return html`<!doctype html>
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
${head}
</head>
<body style="display:flex;flex-direction:row;justify-content:center;">
${bodyContent}
<body>
${body}
</body>
</html>`;
}

View File

@@ -37,7 +37,7 @@ export class BaseDeviceStage<
display: flex;
gap: 16px;
margin-top: 0;
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
margin-bottom: var(--pf-c-form__group--m-action--MarginTop);
flex-direction: column;
}
`,

View File

@@ -55,7 +55,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
display: flex;
gap: 16px;
margin-top: 0;
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
margin-bottom: var(--pf-c-form__group--m-action--MarginTop);
flex-direction: column;
}
`,

View File

@@ -81,4 +81,22 @@ export const ChallengeTurnstileForce = captchaFactory({
jsUrl: "https://challenges.cloudflare.com/turnstile/v0/api.js",
siteKey: "3x00000000000000000000FF",
interactive: true,
} as CaptchaChallenge);
flowInfo: {
layout: "stacked",
cancelUrl: "",
title: "Foo",
},
});
export const ChallengeRecaptcha = captchaFactory({
pendingUser: "foo",
pendingUserAvatar: "https://picsum.photos/64",
jsUrl: "https://www.google.com/recaptcha/api.js",
siteKey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
interactive: true,
flowInfo: {
layout: "stacked",
cancelUrl: "",
title: "Foo",
},
});

View File

@@ -1,21 +1,18 @@
/// <reference types="@hcaptcha/types"/>
/// <reference types="turnstile-types"/>
import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify";
import "@goauthentik/elements/EmptyState";
import { akEmptyState } from "@goauthentik/elements/EmptyState";
import { bound } from "@goauthentik/elements/decorators/bound";
import "@goauthentik/elements/forms/FormElement";
import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe";
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
import { randomId } from "@goauthentik/elements/utils/randomId";
import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { P, match } from "ts-pattern";
import { pluckErrorDetail } from "#common/errors/network";
import { akEmptyState } from "#elements/EmptyState";
import "#elements/forms/FormElement";
import { ListenerController } from "#elements/utils/listenerController";
import { randomId } from "#elements/utils/randomId";
import "#flow/FormStatic";
import { BaseStage } from "#flow/stages/base";
import { CaptchaHandler, iframeTemplate } from "#flow/stages/captcha/shared";
import { match } from "ts-pattern";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { CSSResult, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { createRef, ref } from "lit/directives/ref.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@@ -25,209 +22,161 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
type TokenHandler = (token: string) => void;
export type TokenListener = (token: string) => void;
type Dims = { height: number };
type IframeCaptchaMessage = {
interface CaptchaMessage {
source?: string;
context?: string;
message: "captcha";
token: string;
};
}
type IframeResizeMessage = {
interface LoadMessage {
source?: string;
context?: string;
message: "resize";
size: Dims;
};
type IframeMessageEvent = MessageEvent<IframeCaptchaMessage | IframeResizeMessage>;
type CaptchaHandler = {
name: string;
interactive: () => Promise<unknown>;
execute: () => Promise<unknown>;
refreshInteractive: () => Promise<unknown>;
refresh: () => Promise<unknown>;
};
// A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces
// a resize. Because the Captcha is itself in an iframe, the reported height is often off by some
// margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden
// rendering.
function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult {
return html` ${children}
<script>
new ResizeObserver((entries) => {
const height =
document.body.offsetHeight +
parseFloat(getComputedStyle(document.body).fontSize) * 2;
window.parent.postMessage({
message: "resize",
source: "goauthentik.io",
context: "flow-executor",
size: { height },
});
}).observe(document.querySelector(".ak-captcha-container"));
</script>
<script src=${challengeURL}></script>
<script>
function callback(token) {
window.parent.postMessage({
message: "captcha",
source: "goauthentik.io",
context: "flow-executor",
token,
});
}
</script>`;
message: "load";
}
type IframeMessageEvent = MessageEvent<CaptchaMessage | LoadMessage>;
@customElement("ak-stage-captcha")
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [
PFBase,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
css`
iframe {
width: 100%;
height: 0;
static styles: CSSResult[] = [
PFBase,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
css`
:host {
--captcha-background-to: var(--pf-global--BackgroundColor--light-100);
--captcha-background-from: var(--pf-global--BackgroundColor--light-300);
}
:host([theme="dark"]) {
--captcha-background-to: var(--ak-dark-background-light);
--captcha-background-from: var(--ak-dark-background-light-ish);
}
@keyframes captcha-background-animation {
0% {
background-color: var(--captcha-background-from);
}
`,
];
}
50% {
background-color: var(--captcha-background-to);
}
100% {
background-color: var(--captcha-background-from);
}
}
#ak-captcha {
width: 100%;
min-height: 65px;
&[data-ready="loading"] {
background-color: var(--captcha-background-from);
animation: captcha-background-animation 1s infinite
var(--pf-global--TimingFunction);
}
}
`,
];
//#region Properties
@property({ type: Boolean })
embedded = false;
public embedded = false;
@property()
onTokenChange: TokenHandler = (token: string) => {
public onTokenChange: TokenListener = (token: string) => {
this.host.submit({ component: "ak-stage-captcha", token });
};
@property()
public onLoad?: () => void;
@property({ attribute: false })
refreshedAt = new Date();
public refreshedAt = new Date();
//#endregion
//#region State
@state()
activeHandler?: CaptchaHandler = undefined;
protected activeHandler: CaptchaHandler | null = null;
@state()
error?: string;
protected error: string | null = null;
handlers: CaptchaHandler[] = [
{
name: "grecaptcha",
interactive: this.renderGReCaptchaFrame,
execute: this.executeGReCaptcha,
refreshInteractive: this.refreshGReCaptchaFrame,
refresh: this.refreshGReCaptcha,
},
{
name: "hcaptcha",
interactive: this.renderHCaptchaFrame,
execute: this.executeHCaptcha,
refreshInteractive: this.refreshHCaptchaFrame,
refresh: this.refreshHCaptcha,
},
{
name: "turnstile",
interactive: this.renderTurnstileFrame,
execute: this.executeTurnstile,
refreshInteractive: this.refreshTurnstileFrame,
refresh: this.refreshTurnstile,
},
];
@state()
protected iframeHeight = 65;
_captchaFrame?: HTMLIFrameElement;
_captchaDocumentContainer?: HTMLDivElement;
_listenController = new ListenerController();
#scriptElement?: HTMLScriptElement;
connectedCallback(): void {
super.connectedCallback();
window.addEventListener("message", this.onIframeMessage, {
signal: this._listenController.signal,
});
}
#iframeSource = "about:blank";
#iframeRef = createRef<HTMLIFrameElement>();
disconnectedCallback(): void {
this._listenController.abort();
if (!this.challenge?.interactive) {
if (document.body.contains(this.captchaDocumentContainer)) {
document.body.removeChild(this.captchaDocumentContainer);
}
#iframeLoaded = false;
#captchaDocumentContainer?: HTMLDivElement;
#listenController = new ListenerController();
//#endregion
//#region Getters/Setters
protected get captchaDocumentContainer(): HTMLDivElement {
if (this.#captchaDocumentContainer) {
return this.#captchaDocumentContainer;
}
super.disconnectedCallback();
this.#captchaDocumentContainer = document.createElement("div");
this.#captchaDocumentContainer.id = `ak-captcha-${randomId()}`;
return this.#captchaDocumentContainer;
}
get captchaDocumentContainer(): HTMLDivElement {
if (this._captchaDocumentContainer) {
return this._captchaDocumentContainer;
}
this._captchaDocumentContainer = document.createElement("div");
this._captchaDocumentContainer.id = `ak-captcha-${randomId()}`;
return this._captchaDocumentContainer;
}
//#endregion
get captchaFrame(): HTMLIFrameElement {
if (this._captchaFrame) {
return this._captchaFrame;
}
this._captchaFrame = document.createElement("iframe");
this._captchaFrame.src = "about:blank";
this._captchaFrame.id = `ak-captcha-${randomId()}`;
return this._captchaFrame;
}
onFrameResize({ height }: Dims) {
this.captchaFrame.style.height = `${height}px`;
}
//#region Listeners
// ADR: Did not to put anything into `otherwise` or `exhaustive` here because iframe messages
// that were not of interest to us also weren't necessarily corrupt or suspicious. For example,
// during testing Storybook throws a lot of cross-iframe messages that we don't care about.
@bound
onIframeMessage({ data }: IframeMessageEvent) {
match(data)
.with(
{ source: "goauthentik.io", context: "flow-executor", message: "captcha" },
({ token }) => this.onTokenChange(token),
)
.with(
{ source: "goauthentik.io", context: "flow-executor", message: "resize" },
({ size }) => this.onFrameResize(size),
)
.with(
{ source: "goauthentik.io", context: "flow-executor", message: P.any },
({ message }) => {
console.debug(`authentik/stages/captcha: Unknown message: ${message}`);
},
)
.otherwise(() => {});
}
#messageListener = ({ data }: IframeMessageEvent) => {
if (!data) return;
async renderGReCaptchaFrame() {
this.renderFrame(
html`<div
class="g-recaptcha ak-captcha-container"
data-sitekey="${this.challenge.siteKey}"
data-callback="callback"
></div>`,
);
}
if (data.source !== "goauthentik.io" || data.context !== "flow-executor") {
return;
}
return match(data)
.with({ message: "captcha" }, ({ token }) => this.onTokenChange(token))
.with({ message: "load" }, this.#loadListener)
.otherwise(({ message }) => {
console.debug(`authentik/stages/captcha: Unknown message: ${message}`);
});
};
//#endregion
//#region g-recaptcha
protected renderGReCaptchaFrame = () => {
return html`<div
id="ak-container"
class="g-recaptcha"
data-theme="${this.activeTheme}"
data-sitekey="${this.challenge.siteKey}"
data-callback="callback"
></div>`;
};
async executeGReCaptcha() {
return grecaptcha.ready(() => {
grecaptcha.execute(
return grecaptcha.execute(
grecaptcha.render(this.captchaDocumentContainer, {
sitekey: this.challenge.siteKey,
callback: this.onTokenChange,
@@ -238,7 +187,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
}
async refreshGReCaptchaFrame() {
(this.captchaFrame.contentWindow as typeof window)?.grecaptcha.reset();
this.#iframeRef.value?.contentWindow?.grecaptcha.reset();
}
async refreshGReCaptcha() {
@@ -246,19 +195,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
window.grecaptcha.execute();
}
async renderHCaptchaFrame() {
this.renderFrame(
html`<div
class="h-captcha ak-captcha-container"
data-sitekey="${this.challenge.siteKey}"
data-theme="${this.activeTheme ? this.activeTheme : "light"}"
data-callback="callback"
></div> `,
);
}
//#endregion
//#region h-captcha
protected renderHCaptchaFrame = () => {
return html`<div
id="ak-container"
class="h-captcha"
data-sitekey="${this.challenge.siteKey}"
data-theme="${this.activeTheme}"
data-callback="callback"
></div>`;
};
async executeHCaptcha() {
return hcaptcha.execute(
await hcaptcha.execute(
hcaptcha.render(this.captchaDocumentContainer, {
sitekey: this.challenge.siteKey,
callback: this.onTokenChange,
@@ -268,7 +220,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
}
async refreshHCaptchaFrame() {
(this.captchaFrame.contentWindow as typeof window)?.hcaptcha.reset();
this.#iframeRef.value?.contentWindow?.hcaptcha?.reset();
}
async refreshHCaptcha() {
@@ -276,61 +228,87 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
window.hcaptcha.execute();
}
async renderTurnstileFrame() {
this.renderFrame(
html`<div
class="cf-turnstile ak-captcha-container"
data-sitekey="${this.challenge.siteKey}"
data-callback="callback"
></div>`,
);
}
//#endregion
//#region Turnstile
protected renderTurnstileFrame = () => {
return html`<div
id="ak-container"
class="cf-turnstile"
data-sitekey="${this.challenge.siteKey}"
data-theme="${this.activeTheme}"
data-callback="callback"
data-size="flexible"
></div>`;
};
async executeTurnstile() {
return window.turnstile.render(this.captchaDocumentContainer, {
window.turnstile.render(this.captchaDocumentContainer, {
sitekey: this.challenge.siteKey,
callback: this.onTokenChange,
});
}
async refreshTurnstileFrame() {
(this.captchaFrame.contentWindow as typeof window)?.turnstile.reset();
this.#iframeRef.value?.contentWindow?.turnstile.reset();
}
async refreshTurnstile() {
window.turnstile.reset();
}
async renderFrame(captchaElement: TemplateResult) {
const { contentDocument } = this.captchaFrame || {};
//#endregion
if (!contentDocument) {
console.debug(
"authentik/stages/captcha: unable to render captcha frame, no contentDocument",
);
#handlers = new Map<string, CaptchaHandler>([
[
"grecaptcha",
{
interactive: this.renderGReCaptchaFrame,
execute: this.executeGReCaptcha,
refreshInteractive: this.refreshGReCaptchaFrame,
refresh: this.refreshGReCaptcha,
},
],
[
"hcaptcha",
{
interactive: this.renderHCaptchaFrame,
execute: this.executeHCaptcha,
refreshInteractive: this.refreshHCaptchaFrame,
refresh: this.refreshHCaptcha,
},
],
[
"turnstile",
{
interactive: this.renderTurnstileFrame,
refreshInteractive: this.refreshTurnstileFrame,
execute: this.executeTurnstile,
refresh: this.refreshTurnstile,
},
],
]);
return;
}
contentDocument.open();
contentDocument.write(
createIFrameHTMLWrapper(
renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)),
),
);
contentDocument.close();
}
//#region Render
renderBody() {
// [hasError, isInteractive]
// prettier-ignore
return match([Boolean(this.error), Boolean(this.challenge?.interactive)])
.with([true, P.any], () => akEmptyState({ icon: "fa-times", header: this.error }))
.with([false, true], () => html`${this.captchaFrame}`)
.with([false, false], () => akEmptyState({ loading: true, header: msg("Verifying...") }))
.exhaustive();
if (this.error) {
return akEmptyState({ icon: "fa-times", header: this.error });
}
if (this.challenge?.interactive) {
return html`
<iframe
${ref(this.#iframeRef)}
style="height: ${this.iframeHeight}px;"
data-ready="${this.#iframeLoaded ? "ready" : "loading"}"
id="ak-captcha"
></iframe>
`;
}
return akEmptyState({ loading: true, header: msg("Verifying...") });
}
renderMain() {
@@ -359,76 +337,165 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
}
render() {
// [isEmbedded, hasChallenge, isInteractive]
// prettier-ignore
return match([this.embedded, Boolean(this.challenge), Boolean(this.challenge?.interactive)])
.with([true, false, P.any], () => nothing)
.with([true, true, false], () => nothing)
.with([true, true, true], () => this.renderBody())
.with([false, false, P.any], () => akEmptyState({ loading: true }))
.with([false, true, P.any], () => this.renderMain())
.exhaustive();
if (!this.challenge) {
return this.embedded ? nothing : akEmptyState({ loading: true });
}
if (!this.embedded) {
return this.renderMain();
}
return this.challenge.interactive ? this.renderBody() : nothing;
}
firstUpdated(changedProperties: PropertyValues<this>) {
if (!(changedProperties.has("challenge") && this.challenge !== undefined)) {
//#endregion;
//#region Lifecycle
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("message", this.#messageListener, {
signal: this.#listenController.signal,
});
}
public disconnectedCallback(): void {
this.#listenController.abort();
if (!this.challenge?.interactive) {
if (document.body.contains(this.captchaDocumentContainer)) {
document.body.removeChild(this.captchaDocumentContainer);
}
}
super.disconnectedCallback();
}
//#endregion
public firstUpdated(changedProperties: PropertyValues<this>) {
if (!(changedProperties.has("challenge") && typeof this.challenge !== "undefined")) {
return;
}
const attachCaptcha = async () => {
console.debug("authentik/stages/captcha: script loaded");
const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name));
let lastError = undefined;
let found = false;
for (const handler of handlers) {
console.debug(`authentik/stages/captcha: trying handler ${handler.name}`);
try {
const runner = this.challenge.interactive
? handler.interactive
: handler.execute;
await runner.apply(this);
console.debug(`authentik/stages/captcha[${handler.name}]: handler succeeded`);
found = true;
this.activeHandler = handler;
break;
} catch (exc) {
console.debug(`authentik/stages/captcha[${handler.name}]: handler failed`);
console.debug(exc);
lastError = exc;
}
}
this.error = found ? undefined : (lastError ?? "Unspecified error").toString();
};
this.#refreshVendor();
}
public updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("refreshedAt") || !this.challenge) {
return;
}
if (!this.activeHandler) {
return;
}
console.debug("authentik/stages/captcha: refresh triggered");
this.#run(this.activeHandler);
}
#refreshVendor() {
this.#scriptElement?.remove();
const scriptElement = document.createElement("script");
scriptElement.src = this.challenge.jsUrl;
scriptElement.async = true;
scriptElement.defer = true;
scriptElement.dataset.akCaptchaScript = "true";
scriptElement.onload = attachCaptcha;
scriptElement.onload = this.#scriptLoadListener;
document.head
.querySelectorAll("[data-ak-captcha-script=true]")
.forEach((el) => el.remove());
this.#scriptElement?.remove();
document.head.appendChild(scriptElement);
this.#scriptElement = document.head.appendChild(scriptElement);
if (!this.challenge.interactive) {
document.body.appendChild(this.captchaDocumentContainer);
}
}
updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("refreshedAt") || !this.challenge) {
//#endregion
//#region Listeners
#loadListener = () => {
const iframe = this.#iframeRef.value;
const contentDocument = iframe?.contentDocument;
if (!iframe || !contentDocument) return;
const resizeListener: ResizeObserverCallback = () => {
if (!this.#iframeRef) return;
const target = contentDocument.getElementById("ak-container");
if (!target) return;
this.iframeHeight = Math.round(target.clientHeight);
};
const resizeObserver = new ResizeObserver(resizeListener);
requestAnimationFrame(() => {
resizeObserver.observe(contentDocument.body);
this.onLoad?.();
this.#iframeLoaded = true;
});
};
#scriptLoadListener = async (): Promise<void> => {
console.debug("authentik/stages/captcha: script loaded");
this.error = null;
this.#iframeLoaded = false;
for (const [name, handler] of this.#handlers) {
if (!Object.hasOwn(window, name)) {
continue;
}
try {
await this.#run(handler);
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
this.activeHandler = handler;
return;
} catch (error) {
console.debug(`authentik/stages/captcha[${name}]: handler failed`);
console.debug(error);
this.error = pluckErrorDetail(error, "Unspecified error");
}
}
};
async #run(handler: CaptchaHandler) {
if (this.challenge.interactive) {
const iframe = this.#iframeRef.value;
if (!iframe) {
console.debug(`authentik/stages/captcha: No iframe found, skipping.`);
return;
}
console.debug(`authentik/stages/captcha: Rendering interactive.`);
const captchaElement = handler.interactive();
const template = iframeTemplate(captchaElement, this.challenge.jsUrl);
URL.revokeObjectURL(this.#iframeSource);
const url = URL.createObjectURL(new Blob([template], { type: "text/html" }));
this.#iframeSource = url;
iframe.src = url;
return;
}
console.debug("authentik/stages/captcha: refresh triggered");
if (this.challenge.interactive) {
this.activeHandler?.refreshInteractive.apply(this);
} else {
this.activeHandler?.refresh.apply(this);
}
await handler.execute.apply(this);
}
}

View File

@@ -0,0 +1,11 @@
/// <reference types="@types/grecaptcha"/>
export {};
declare global {
interface Window {
grecaptcha: ReCaptchaV2.ReCaptcha & {
enterprise: ReCaptchaV2.ReCaptcha;
};
}
}

View File

@@ -0,0 +1,9 @@
/// <reference types="@hcaptcha/types"/>
export {};
declare global {
interface Window {
hcaptcha?: HCaptcha;
}
}

View File

@@ -0,0 +1,65 @@
import { createDocumentTemplate } from "#elements/utils/iframe";
import { TemplateResult, html } from "lit";
export interface CaptchaHandler {
interactive(): TemplateResult;
execute(): Promise<void>;
refreshInteractive(): Promise<void>;
refresh(): Promise<void>;
}
/**
* A container iframe for a hosted Captcha, with an event emitter to monitor
* when the Captcha forces a resize.
*
* Because the Captcha is itself in an iframe, the reported height is often off by some
* margin, adding 2rem of height to our container adds padding and prevents scrollbars
* or hidden rendering.
*/
export function iframeTemplate(children: TemplateResult, challengeURL: string): string {
return createDocumentTemplate({
head: html`<meta charset="UTF-8" />
<script>
"use strict";
function callback(token) {
self.parent.postMessage({
message: "captcha",
source: "goauthentik.io",
context: "flow-executor",
token,
});
}
function loadListener() {
self.parent.postMessage({
message: "load",
source: "goauthentik.io",
context: "flow-executor",
});
}
</script>
<style>
body {
margin: 0;
padding: 0;
}
.g-recaptcha {
padding-block: 0.5rem;
}
.g-recaptcha,
.h-captcha {
display: flex;
align-items: center;
justify-content: center;
}
</style>`,
body: html`${children}
<script onload="loadListener()" src="${challengeURL}"></script> `,
});
}

View File

@@ -0,0 +1,9 @@
/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference types="turnstile-types"/>
import { TurnstileObject } from "turnstile-types";
declare global {
interface Window {
turnstile: TurnstileObject;
}
}

View File

@@ -10,6 +10,7 @@ import { AkRememberMeController } from "@goauthentik/flow/stages/identification/
import { msg, str } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@@ -54,6 +55,8 @@ export class IdentificationStage extends BaseStage<
captchaToken = "";
@state()
captchaRefreshedAt = new Date();
@state()
captchaLoaded = false;
static get styles(): CSSResult[] {
return [
@@ -81,16 +84,42 @@ export class IdentificationStage extends BaseStage<
height: 100%;
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
}
.captcha-container {
position: relative;
.faux-input {
position: absolute;
bottom: 0;
left: 0;
opacity: 0;
pointer-events: none;
}
}
`,
];
}
#captchaInputRef = createRef<HTMLInputElement>();
#tokenChangeListener = (token: string) => {
const input = this.#captchaInputRef.value;
if (!input) return;
input.value = token;
};
#captchaLoadListener = () => {
this.captchaLoaded = true;
};
constructor() {
super();
this.rememberMe = new AkRememberMeController(this);
}
updated(changedProperties: PropertyValues<this>) {
public updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("challenge") && this.challenge !== undefined) {
this.autoRedirect();
this.createHelperForm();
@@ -162,9 +191,12 @@ export class IdentificationStage extends BaseStage<
input.focus();
});
};
this.form.appendChild(password);
}
const totp = document.createElement("input");
totp.setAttribute("type", "text");
totp.setAttribute("name", "code");
totp.setAttribute("autocomplete", "one-time-code");
@@ -298,19 +330,33 @@ export class IdentificationStage extends BaseStage<
${this.renderNonFieldErrors()}
${this.challenge.captchaStage
? html`
<input name="captchaToken" type="hidden" .value="${this.captchaToken}" />
<ak-stage-captcha
.challenge=${this.challenge.captchaStage}
.onTokenChange=${(token: string) => {
this.captchaToken = token;
}}
.refreshedAt=${this.captchaRefreshedAt}
embedded
></ak-stage-captcha>
<div class="captcha-container">
<ak-stage-captcha
.challenge=${this.challenge.captchaStage}
.onTokenChange=${this.#tokenChangeListener}
.onLoad=${this.#captchaLoadListener}
.refreshedAt=${this.captchaRefreshedAt}
embedded
>
</ak-stage-captcha>
<input
class="faux-input"
${ref(this.#captchaInputRef)}
name="captchaToken"
type="text"
required
value=""
/>
</div>
`
: nothing}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
<div class="pf-c-form__group ${this.challenge.captchaStage ? "" : "pf-m-action"}">
<button
?disabled=${this.challenge.captchaStage && !this.captchaLoaded}
type="submit"
class="pf-c-button pf-m-primary pf-m-block"
>
${this.challenge.primaryAction}
</button>
</div>

View File

@@ -1,9 +1,26 @@
import { SubmitOptions } from "#flow/stages/base";
import { FlowExecutor } from "@goauthentik/flow/FlowExecutor";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { FlowChallengeResponseRequest } from "@goauthentik/api";
@customElement("ak-storybook-interface-flow")
export class StoryFlowInterface extends FlowExecutor {}
export class StoryFlowInterface extends FlowExecutor {
async firstUpdated() {}
submit = async (
payload?: FlowChallengeResponseRequest,
options?: SubmitOptions,
): Promise<boolean> => {
return true;
};
async renderChallenge(): Promise<TemplateResult> {
return html`<slot></slot>`;
}
}
declare global {
interface HTMLElementTagNameMap {

View File

@@ -11,7 +11,7 @@ import { StageHost } from "#flow/stages/base";
import "#user/user-settings/details/stages/prompt/PromptStage";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
@@ -36,7 +36,7 @@ export class UserSettingsFlowExecutor
implements StageHost
{
@property()
flowSlug?: string;
flowSlug = this.brand?.flowUserSettings;
private _challenge?: ChallengeTypes;
@@ -86,12 +86,15 @@ export class UserSettingsFlowExecutor
});
}
updated(changedProperties: PropertyValues<this>): void {
if (changedProperties.has("brand") && this.brand) {
firstUpdated() {
if (this.flowSlug) {
this.nextChallenge();
}
}
updated(): void {
if (!this.flowSlug && this.brand?.flowUserSettings) {
this.flowSlug = this.brand.flowUserSettings;
if (!this.flowSlug) return;
this.nextChallenge();
}
}

View File

@@ -9106,9 +9106,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9246,6 +9243,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -7608,9 +7608,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -7748,6 +7745,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -9167,9 +9167,6 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9307,6 +9304,18 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -9690,10 +9690,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<source>Failed to preview prompt</source>
<target>Échec de la prévisualisation de l'invite</target>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
<target>Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...'. Lorsque "Recherche avec un attribut utilisateur" est sélectionné, cet attribut doit être un attribut utilisateur, sinon un attribut de groupe.</target>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
<target>Recherche avec un attribut utilisateur</target>
@@ -9877,6 +9873,18 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
<target>Supprimer les utilisateurs et les groupes authentik qui étaient auparavant fournis par cette source, mais qui en sont maintenant absents.</target>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -9690,10 +9690,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Failed to preview prompt</source>
<target>Impossibile visualizzare l'anteprima del prompt</target>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
<target>Campo che contiene i membri di un gruppo. Si noti che se si utilizza il campo "memberUid", si presume che il valore contenga un nome relativo distinto. Ad esempio, "memberUid=some-user" invece di "memberUid=cn=some-user,ou=groups,...". Quando si seleziona "Cerca utilizzando un attributo utente", questo dovrebbe essere un attributo utente, altrimenti un attributo di gruppo.</target>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
<target>Ricerca tramite attributo utente</target>
@@ -9860,6 +9856,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -9075,9 +9075,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9215,6 +9212,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -8977,9 +8977,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9117,6 +9114,18 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -9402,9 +9402,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9542,6 +9539,18 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -9409,9 +9409,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9550,4 +9547,16 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body></file></xliff>

View File

@@ -9494,9 +9494,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9634,6 +9631,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -9465,9 +9465,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9605,6 +9602,18 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -6215,9 +6215,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -6356,6 +6353,18 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body>
<trans-unit id="s4caed5b7a7e5d89b">
@@ -596,9 +596,9 @@
</trans-unit>
<trans-unit id="saa0e2675da69651b">
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source>
<target>未找到 URL &quot;
<x id="0" equiv-text="${this.url}"/>&quot;。</target>
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>未找到 URL "
<x id="0" equiv-text="${this.url}"/>"。</target>
</trans-unit>
<trans-unit id="s58cd9c2fe836d9c6">
@@ -1715,8 +1715,8 @@
</trans-unit>
<trans-unit id="sa90b7809586c35ce">
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon &quot;fa-test&quot;.</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 &quot;fa-test&quot;。</target>
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
</trans-unit>
<trans-unit id="s0410779cb47de312">
@@ -3778,10 +3778,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sa95a538bfbb86111">
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</source>
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
<target>您确定要更新
<x id="0" equiv-text="${this.objectLabel}"/>&quot;
<x id="1" equiv-text="${this.obj?.name}"/>&quot; 吗?</target>
<x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
</trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6">
@@ -4847,7 +4847,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sdf1d8edef27236f0">
<source>A &quot;roaming&quot; authenticator, like a YubiKey</source>
<source>A "roaming" authenticator, like a YubiKey</source>
<target>像 YubiKey 这样的“漫游”身份验证器</target>
</trans-unit>
@@ -5206,7 +5206,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s1608b2f94fa0dbd4">
<source>If set to a duration above 0, the user will have the option to choose to &quot;stay signed in&quot;, which will extend their session by the time specified here.</source>
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
<target>如果设置时长大于 0用户可以选择“保持登录”选项这将使用户的会话延长此处设置的时间。</target>
</trans-unit>
@@ -7492,7 +7492,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
</trans-unit>
<trans-unit id="s824e0943a7104668">
<source>This user will be added to the group &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</source>
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
<target>此用户将会被添加到组 &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;。</target>
</trans-unit>
<trans-unit id="s62e7f6ed7d9cb3ca">
@@ -8778,7 +8778,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>同步组</target>
</trans-unit>
<trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${p.name}"/> (&quot;<x id="1" equiv-text="${p.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${p.type}"/>)</source>
<source><x id="0" equiv-text="${p.name}"/> ("<x id="1" equiv-text="${p.fieldKey}"/>", of type <x id="2" equiv-text="${p.type}"/>)</source>
<target><x id="0" equiv-text="${p.name}"/>&amp;quot;<x id="1" equiv-text="${p.fieldKey}"/>&amp;quot;,类型为 <x id="2" equiv-text="${p.type}"/></target>
</trans-unit>
<trans-unit id="s25bacc19d98b444e">
@@ -9026,8 +9026,8 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>授权流程成功后有效的重定向 URI。还可以在此处为隐式流程指定任何来源。</target>
</trans-unit>
<trans-unit id="s4c49d27de60a532b">
<source>To allow any redirect URI, set the mode to Regex and the value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请设置模式为正则表达式并将此值设置为 &quot;.*&quot;。请注意这可能带来的安全影响。</target>
<source>To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请设置模式为正则表达式并将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
</trans-unit>
<trans-unit id="sa52bf79fe1ccb13e">
<source>Federated OIDC Sources</source>
@@ -9691,10 +9691,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Failed to preview prompt</source>
<target>预览输入失败</target>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the &quot;memberUid&quot; field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
<target>包含组成员的字段。请注意,如果使用 &quot;memberUid&quot; 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'。当选中“使用用户属性查询”时,此配置应该为用户属性,否则为组属性。</target>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
<target>使用用户属性查询</target>
@@ -9784,7 +9780,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>在 authorization_code 令牌请求流程期间,如何执行身份验证</target>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable &quot;Remember me on this device&quot;</source>
<source>Enable "Remember me on this device"</source>
<target>启用“在此设备上记住我”</target>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
@@ -9878,7 +9874,19 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
<target>删除之前由此源提供,但现已缺失的用户和组。</target>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>
</xliff>
</xliff>

View File

@@ -7308,9 +7308,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -7448,6 +7445,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

View File

@@ -9052,9 +9052,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7524ea24eeeb019">
<source>Failed to preview prompt</source>
</trans-unit>
<trans-unit id="s783964a224796865">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'. When selecting 'Lookup using a user attribute', this should be a user attribute, otherwise a group attribute.</source>
</trans-unit>
<trans-unit id="s1d47b4f61ca53e8e">
<source>Lookup using user attribute</source>
</trans-unit>
@@ -9192,6 +9189,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="se3b26b762110bda0">
<source>Delete authentik users and groups which were previously supplied by this source, but are now missing from it.</source>
</trans-unit>
<trans-unit id="s0a2cb398b54a6207">
<source>Welcome.</source>
</trans-unit>
<trans-unit id="s4e1d2cb86cf5ecd0">
<source>Field which contains members of a group. The value of this field is matched against User membership attribute.</source>
</trans-unit>
<trans-unit id="s6478025f3e0174fa">
<source>User membership attribute</source>
</trans-unit>
<trans-unit id="s344be99cf5d36407">
<source>Attribute which matches the value of Group membership field.</source>
</trans-unit>
</body>
</file>

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