Compare commits

..

93 Commits

Author SHA1 Message Date
authentik-automation[bot]
f83d3a19d0 release: 2025.12.0-rc3 2026-01-06 19:51:06 +00:00
authentik-automation[bot]
ef59ff1856 web: Fix user library colors, modal z-indexes, table progress bars (cherry-pick #19152 to version-2025.12) (#19174)
Cherry-pick #19152 to version-2025.12 (with conflicts)
This cherry-pick has conflicts that need manual resolution.

Original PR: #19152
Original commit: 3838150

Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
2026-01-06 19:15:39 +00:00
authentik-automation[bot]
4966225282 outpost/proxyv2: reduce max number of postgres connections (cherry-pick #19211 to version-2025.12) (#19214)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-01-06 19:20:39 +01:00
authentik-automation[bot]
2b8765d0aa web: fix promoted source button hover losing blue color (cherry-pick #19048 to version-2025.12) (#19100)
web: fix promoted source button hover losing blue color (#19048)

Co-authored-by: Dominic R <dominic@sdko.org>
2026-01-06 18:10:50 +00:00
authentik-automation[bot]
d60d06f958 core: handle deserialization errors from FileField migration (cherry-pick #19067 to version-2025.12) (#19168)
Co-authored-by: Dominic R <dominic@sdko.org>
2026-01-06 18:48:35 +01:00
authentik-automation[bot]
1a3f268476 admin/files: support %(theme)s variable in media file paths (cherry-pick #19108 to version-2025.12) (#19213)
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-01-06 14:32:12 +01:00
authentik-automation[bot]
515a855c40 web/admin: adjust sync threshold, add tooltip (cherry-pick #19131 to version-2025.12) (#19175)
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-01-06 13:48:37 +01:00
authentik-automation[bot]
16d65b8d12 rbac: Add show all to roles tab, add role tab to groups (cherry-pick #19097 to version-2025.12) (#19199)
rbac: Add show all to roles tab, add role tab to groups (#19097)

* improve sort order and inherit visual

* Update web/src/admin/groups/GroupViewPage.ts




* Update web/src/admin/users/UserViewPage.ts




* Update web/src/admin/roles/RelatedRoleList.ts




* Update web/src/admin/roles/RelatedRoleList.ts




* Update web/src/admin/roles/RelatedRoleList.ts




* Update web/src/admin/roles/RelatedRoleList.ts




* setup include inherited roles and fix returning nothing

* update api calls

* fix rendering error

* do not use set

* change from exception handling

* go off query param

* fix wording

* fix linting error for new group api structure

---------

Signed-off-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2026-01-06 04:01:06 +00:00
authentik-automation[bot]
bfe928df18 web: Defer table refresh, visibility checks. (cherry-pick #19194 to version-2025.12) (#19198)
* web: Fix user library colors, modal z-indexes, table progress bars (#19152)

* Fix progress bar fade out, positioning, labels.

* Export parts. Fix z-index, colors.

* Fix clickable area.

* Ignore clickable icons.

* web: Defer table refresh, visibility checks. (#19194)

Fix types, args.

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-01-05 23:38:47 +00:00
authentik-automation[bot]
c447bbe6c8 web: Merge branch -- Stale notifications, synchronized context objects, rendering fixes (cherry-pick #19141 to version-2025.12) (#19197)
Cherry-pick #19141 to version-2025.12 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #19141
Original commit: 2c813cbe03

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-01-05 23:13:08 +00:00
authentik-automation[bot]
1c0a3f95df core: add prettier failure on duplicate group names (cherry-pick #18941 to version-2025.12) (#19193)
core: add prettier failure on duplicate group names (#18941)

* core: add prettier failure on duplicate group names

* add db_alias




* lint

* migrate to system migration



* fix error on empty database



* returning a count of 0 still takes 1 row :P

---------

Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-01-05 18:44:03 +01:00
authentik-automation[bot]
8a6116ab79 lifecycle: fix migration conn_options for psycopg connection (cherry-pick #19134 to version-2025.12) (#19186)
Co-authored-by: Duncan Tasker <72562945+D-Tasker207@users.noreply.github.com>
fix migration conn_options for psycopg connection (#19134)
2026-01-05 15:21:49 +01:00
authentik-automation[bot]
430010fbea website/docs: remove duplicates in slo docs (cherry-pick #19170 to version-2025.12) (#19177)
website/docs: remove duplicates in slo docs (#19170)

remove duplicated points in the iframe mode points in slo docs

Co-authored-by: Adithya S Narasinghe <adithyasnarasinghe@gmail.com>
2026-01-04 21:41:54 +00:00
authentik-automation[bot]
079b575a45 web: fix slug auto-updating when editing existing applications (cherry-pick #19169 to version-2025.12) (#19173)
web: fix slug auto-updating when editing existing applications (#19169)

Co-authored-by: Dominic R <dominic@sdko.org>
2026-01-04 04:23:21 +00:00
authentik-automation[bot]
b2ca887d59 website/docs: endpoint agent release notes (cherry-pick #19042 to version-2025.12) (#19146)
website/docs: endpoint agent release notes (#19042)

* website/docs: endpoint agent release notes



* Apply suggestion from @dominic-r



* rename, update



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-01-03 21:35:39 +01:00
authentik-automation[bot]
d7b30ad0d7 web: Token Form Fixes (cherry-pick #19121 to version-2025.12) (#19153)
web: Token Form Fixes (#19121)

* Fix autofocus attribute.

* web: Fix label alignment, focus handlers, edit states.

* Tidy date functions.

* Use Dates over strings.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-01-03 02:20:12 +00:00
authentik-automation[bot]
b084ace1dd web/user: fix consent delete form missing details (cherry-pick #19147 to version-2025.12) (#19156)
web/user: fix consent delete form missing details (#19147)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-01-02 12:27:56 +01:00
authentik-automation[bot]
b3e45cdf1a website/docs: fix build (cherry-pick #19148 to version-2025.12) (#19151)
website/docs: fix build (#19148)

* ensure we never throw errors in the browser



* cleaner



* rework



* fix misleading variable



* Tidy behavior.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
2026-01-01 18:53:33 +00:00
authentik-automation[bot]
8132e1f7d9 web: Capitalize language display names, code owner fix (cherry-pick #19119 to version-2025.12) (#19122)
web: Capitalize language display names, code owner fix (#19119)

* web: Capitalize locale display names.

* Fix broad code owner.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-12-30 20:50:42 -05:00
authentik-automation[bot]
149dccf244 web: fix file search input not resetting results properly (cherry-pick #19034 to version-2025.12) (#19075)
web: fix file search input not resetting results properly (#19034)

Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-12-30 20:18:23 +00:00
authentik-automation[bot]
b5e4797761 web: Fix stale flow background (cherry-pick #19015 to version-2025.12) (#19101)
web: Fix stale flow background (#19015)

Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-30 19:31:58 +00:00
authentik-automation[bot]
be670d6253 web: Fix Impersonation, Lit Reactive Controller Contexts (cherry-pick #19114 to version-2025.12) (#19117)
web: Fix Impersonation, Lit Reactive Controller Contexts (#19114)

* web: Fix issue where impersonation does not trigger updates.

* web: Fix issues surrounding abort controller types, lifecycle.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-12-30 17:06:25 +00:00
authentik-automation[bot]
71060ea4e7 website/docs: release notes: Add more integrations (cherry-pick #19109 to version-2025.12) (#19115)
website/docs: release notes: Add more integrations (#19109)

Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-30 08:50:05 +00:00
authentik-automation[bot]
f60f38280c website/docs: endpoints: mention connector key required for stage to work (cherry-pick #19084 to version-2025.12) (#19095)
website/docs: endpoints: mention connector key required for stage to work (#19084)

keypair = CertificateKeyPair.objects.filter(pk=stage.connector.challenge_key_id).first()
  if not keypair:
      return self.executor.stage_ok()  # < --- skips the stage

took me a bit of time to find this and yea

Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-29 20:59:38 +01:00
authentik-automation[bot]
418deeb332 website/docs: endpoint devices: add path to macos setup (cherry-pick #19093 to version-2025.12) (#19099)
website/docs: endpoint devices: add path to macos setup (#19093)

* Add path

* Update macos.md



---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-12-29 20:59:18 +01:00
authentik-automation[bot]
619c77c27e web/admin: use consistent icon for inactive user status (cherry-pick #19032 to version-2025.12) (#19035)
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-29 08:26:45 -08:00
authentik-automation[bot]
ddfddb49da website/docs: endpoint devices: update features table (cherry-pick #19094 to version-2025.12) (#19098)
website/docs: endpoint devices: update features table (#19094)

* Update table

* Remove wording

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-12-29 13:35:29 +00:00
authentik-automation[bot]
dbbb1870b7 website/docs: rel notes .12: add wallos (cherry-pick #19063 to version-2025.12) (#19096)
website/docs: rel notes .12: add wallos (#19063)

Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-29 12:43:03 +00:00
authentik-automation[bot]
5b43301206 docs/release notes: update 2025.12 release notes (cherry-pick #19043 to version-2025.12) (#19046)
docs/release notes: update 2025.12 release notes (#19043)

* Add links and tags

* Update website/docs/releases/2025/v2025.12.md




---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-29 05:09:34 +00:00
authentik-automation[bot]
d915d1a94a web/admin: fix button alignment on user view page (cherry-pick #19079 to version-2025.12) (#19081)
web/admin: fix button alignment on user view page (#19079)

* web/admin: fix button alignment on user view page



* fix width



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-28 19:58:29 +01:00
authentik-automation[bot]
786497790a internal: update TLS Suite (cherry-pick #19076 to version-2025.12) (#19078)
internal: update TLS Suite (#19076)

* internal: update TLS Suite



* disable chacha20 due to fips



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-28 15:19:46 +01:00
authentik-automation[bot]
56c899cf21 blueprints: set enrollment token key (cherry-pick #19061 to version-2025.12) (#19062)
blueprints: set enrollment token key (#19061)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-26 23:42:27 +01:00
authentik-automation[bot]
943f22e5a9 blueprints: fix deadlock and task context error in MetaApplyBlueprint (cherry-pick #19033 to version-2025.12) (#19068)
blueprints: fix deadlock and task context error in MetaApplyBlueprint (#19033)

Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-12-26 23:41:22 +01:00
authentik-automation[bot]
11b45689f4 blueprints: fix flaky tests (cherry-pick #19002 to version-2025.12) (#19059)
blueprints: fix flaky tests (#19002)

* blueprints: attempt to fix tests



* fix postgres debug logging



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-26 16:11:21 +01:00
authentik-automation[bot]
87f443532f endpoints/devices: cleanup (cherry-pick #19047 to version-2025.12) (#19057)
* endpoints/devices: cleanup  (#19047)

* endpoints: make device token internally managed

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

* fix text and defaults for agent

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

* re-org some code

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

---------

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

* fix

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-12-26 15:53:23 +01:00
authentik-automation[bot]
0c672a0c37 lib/sync: fix sync_dispatch (cherry-pick #19053 to version-2025.12) (#19056)
lib/sync: fix sync_dispatch (#19053)

* fix: add missing call to all on self.schedules

Fixes #19051

* fix: change the name of syncOutgoingTriggerMode ak-radio-input

Fixes #19052

Co-authored-by: Amélie Lilith Krejčí <krejcar25@blep.cz>
2025-12-26 14:20:34 +01:00
authentik-automation[bot]
dfd11ceb57 events: notifications live update (cherry-pick #18980 to version-2025.12) (#18990)
events: notifications live update (#18980)

* this has been broken for a while but no one noticed...? cc @rissson



* send WS broadcast for new notifications



* add tests



* better layout



* fix e2e tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-24 14:29:14 +01:00
authentik-automation[bot]
d865b7fd87 core: use chunked_queryset for expired message deletion (cherry-pick #19028 to version-2025.12) (#19031)
core: use chunked_queryset for expired message deletion (#19028)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-24 01:56:23 +01:00
authentik-automation[bot]
aa8a6b9c43 web: Locale selector UI fixes (cherry-pick #18972 to version-2025.12) (#19027)
web: Locale selector UI fixes (#18972)

* Fix alignment, focus.

* Clean up.

* Tidy click area.

* Fix compatibility mode.

* Fix alignment.

* Fix issues surrounding labels, alignment, consistency.

* Update web/src/common/ui/locale/format.ts



* Tidy hover states.

* Tidy.

* Clean up parsing.

* Tidy comments, usage.

* Always use script naming over region.

* Remove unused.

* Spacing.

---------

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-12-23 16:22:32 -05:00
Jens L.
fe5313f42e ci: ensure disk space is available (#19025)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-12-23 17:57:03 +01:00
authentik-automation[bot]
499f739e2b website/docs: Prioritize "Release Candidate" over "Current Release" (cherry-pick #18975 to version-2025.12) (#19022)
website/docs: Prioritize "Release Candidate" over "Current Release" (#18975)

Normalize labels.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-12-23 16:28:23 +00:00
authentik-automation[bot]
4e0e738823 web/admin: prevent file upload attempt when backend not managed (cherry-pick #18646 to version-2025.12) (#19021)
web/admin: prevent file upload attempt when backend not managed (#18646)

* web/admin: prevent file upload attempt when backend not managed



* wip

* fixup

* rework



* format



* add check for reports



* fix delete table for data exports missing details



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-23 14:47:24 +01:00
authentik-automation[bot]
24360bf306 web: fix Open button selecting row instead of navigating (cherry-pick #18992 to version-2025.12) (#19003)
web: fix Open button selecting row instead of navigating (#18992)

the `isEventTargetingListener()` function only checked the click target and the immediate parent for interactive elements (like links, buttons and more). when clicking the icon inside the Open button, the DOM structure is:

<a href=...>  <--- 2 levels up, never checked
<pf-tooltip>  <--- immediate parent, not interactive
<i> <---- click target, not interactive

Because <i> and <pf-tooltip> did not match the interactive elements query, the function returned false which caused the table rowClickListener to continue with row selection isntead of allowing the click.

The fix is to update the function to to traverse (up) the entire dom tree from the click target to the listener element (the table cell) and check for each ancestor for the interactive elements.

Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-22 23:48:07 +01:00
authentik-automation[bot]
6fad3c2bbd enterprise/search: add static autocomplete structure (cherry-pick #19008 to version-2025.12) (#19011)
enterprise/search: add static autocomplete structure (#19008)

* enterprise/search: add static autocomplete structure



* add recursive structured for context



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-22 23:47:44 +01:00
authentik-automation[bot]
2cf20de7ec website/docs: improve endpoint devices docs (cherry-pick #19007 to version-2025.12) (#19012)
website/docs: improve endpoint devices docs (#19007)

* Remove sudo auth sections

* Add firefox extension link

* Add chrome extension

* Update release notes

* Remove link

* Fix link

* Fix release note wording

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-12-22 20:50:05 +00:00
authentik-automation[bot]
3d8d3bb8ce enterprise/reports: improve export list, confirmation (cherry-pick #18981 to version-2025.12) (#19010)
enterprise/reports: improve export list, confirmation (#18981)

* enterprise/reports: use verbose name for model label



* add confirmation for export



* update docs



* remove duplicated api



* fix duplicate



* fix search query not updated



* exclude page & page size



* improve query display



* fix user display



* exclude unset params



* Apply suggestions from code review




* more code style



* format



* fix types



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-12-22 20:53:51 +01:00
authentik-automation[bot]
80bcbe4885 web/admin: Fix haveibeenpwned link in PasswordPolicyForm (cherry-pick #18984 to version-2025.12) (#18989)
web/admin: Fix haveibeenpwned link in PasswordPolicyForm (#18984)

web: Fix haveibeenpwned link in PasswordPolicyForm

Co-authored-by: Henry Skrtich <1214484+hskrtich@users.noreply.github.com>
2025-12-21 15:46:52 +01:00
authentik-automation[bot]
32e4782ed8 web/admin: fix dark theme on map (cherry-pick #18985 to version-2025.12) (#18987)
web/admin: fix dark theme on map (#18985)

web/admin: fix dark theme on map broken

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-21 15:44:39 +01:00
authentik-automation[bot]
613a51bdbb web/admin: fix endpoints user binding (cherry-pick #18935 to version-2025.12) (#18952)
web/admin: fix endpoints user binding (#18935)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-19 17:53:22 +01:00
Teffen Ellis
1c6de43701 website/docs: Backport version picker updates. (#18964)
Fix import path.

Show unlisted entries if release.

Fix sidebar rendering.

Fix positioning of pre-release note. Tidy phrasing.

Clarify pre-release vs draft.

website/docs: Fix version parsing.
2025-12-19 17:07:56 +01:00
authentik-automation[bot]
6771530025 web/admin: add UI copy to RBAC modal (cherry-pick #18917 to version-2025.12) (#18962)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-12-19 16:49:32 +01:00
authentik-automation[bot]
5876f367bc website/docs: add note to active directory source doc (cherry-pick #18787 to version-2025.12) (#18966)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-19 16:48:20 +01:00
authentik-automation[bot]
e263af2dd9 web/elements: progress-bar and table loading header (cherry-pick #18934 to version-2025.12) (#18939)
web/elements: progress-bar and table loading header (#18934)

* add ak-progress-bar



* make intermediate smaller



* add table



* hide table overflow



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-19 00:06:02 +01:00
authentik-automation[bot]
3a59911a2b website/docs: release notes: add endpoint device links to 2025.12 notes (cherry-pick #18940 to version-2025.12) (#18947)
website/docs: release notes: add endpoint device links to 2025.12 notes (#18940)

Add links to release notes

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-12-18 21:59:53 +00:00
authentik-automation[bot]
bbf31e99c3 website/docs: endpoint devices (cherry-pick #18634 to version-2025.12) (#18946)
website/docs: endpoint devices (#18634)

* Initial

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* Apply suggestions from code review




* Apply suggestions from code review




* Apply suggestions

* Apply suggestions

* Apply suggestions from code review




* Apply suggestions from code review




* WIP

* Apply suggestions from code review




* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* fixes



* WIP

* Optimised images with calibre/image-actions

* Optimised images with calibre/image-actions

* Optimised images with calibre/image-actions

* Optimised images with calibre/image-actions

* Optimised images with calibre/image-actions

* Optimised images with calibre/image-actions

* Optimised images with calibre/image-actions

* Fix anchor

* Update website/docs/endpoint-devices/index.mdx




* WIP

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-12-18 19:40:56 +00:00
authentik-automation[bot]
9d5bd42f3e stages/identification: replace sleep with make_password (cherry-pick #18883 to version-2025.12) (#18943)
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-18 19:10:01 +01:00
authentik-automation[bot]
e721dae6da web/flow: Fix spurious double submit on ak-stage-autosubmit (cherry-pick #18727 to version-2025.12) (#18933)
web/flow: Fix spurious double submit  on ak-stage-autosubmit (#18727)

* Fix double submission on ak-stage-autosubmit

* use updated correctly



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Victor Nawothnig <dminuoso@icloud.com>
Co-authored-by: Victor Nawothnig <Victor.Nawothnig+git@icloud.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-12-18 16:25:19 +01:00
authentik-automation[bot]
af3106b144 packages/ak-guardian: cast safely (cherry-pick #18929 to version-2025.12) (#18931)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-18 16:24:59 +01:00
authentik-automation[bot]
5b55103575 tests/e2e: handle StaleElementReferenceException in parse_json_content (cherry-pick #18842 to version-2025.12) (#18919)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-12-18 14:08:49 +01:00
authentik-automation[bot]
ee4ecf929f release: 2025.12.0-rc2 2025-12-17 22:03:04 +00:00
authentik-automation[bot]
8336556a6f root: fix docker-compose data mount (cherry-pick #18903 to version-2025.12) (#18918)
root: fix docker-compose data mount (#18903)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-17 19:53:44 +00:00
authentik-automation[bot]
709aad1d3b core/groups: optimize prefetch queries to fetch only required fields (cherry-pick #18448 to version-2025.12) (#18914)
core/groups: optimize prefetch queries to fetch only required fields (#18448)

Co-authored-by: João C. Fernandes <joaocfernandes@gmail.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-17 17:50:44 +00:00
authentik-automation[bot]
fb7ab4937c web/admin: reword some things on the device view page (cherry-pick #18785 to version-2025.12) (#18913)
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-17 17:55:14 +01:00
authentik-automation[bot]
5df1726d80 website/docs: 2025.12: remove superfluous changes (cherry-pick #18910 to version-2025.12) (#18912)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-17 17:25:13 +01:00
authentik-automation[bot]
9fdb568843 ci/release-tag: checkout correct branch for make test-docker (cherry-pick #18880 to version-2025.12) (#18911)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-17 16:24:56 +01:00
authentik-automation[bot]
8e76f56f89 api: fix latest version for public schema (cherry-pick #18902 to version-2025.12) (#18909)
Co-authored-by: Jens L. <jens@goauthentik.io>
fix latest version for public schema (#18902)
2025-12-17 16:14:16 +01:00
authentik-automation[bot]
05d3791577 website/docs: added list of Int Guide contributors (also edited frontmatter) (cherry-pick #18888 to version-2025.12) (#18907)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-17 16:10:50 +01:00
authentik-automation[bot]
d00dd7eb90 api: fix page_size with invalid query param (cherry-pick #18879 to version-2025.12) (#18908)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix page_size with invalid query param (#18879)
2025-12-17 16:10:07 +01:00
authentik-automation[bot]
8d2e404017 stages/authenticator_*: fix code input field not string (cherry-pick #18875 to version-2025.12) (#18906)
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix code input field not string (#18875)
2025-12-17 16:04:32 +01:00
authentik-automation[bot]
95eb2af25e tasks/middleware: close connections on worker status update database error (cherry-pick #18881 to version-2025.12) (#18905)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
close connections on worker status update database error (#18881)
2025-12-17 15:46:53 +01:00
authentik-automation[bot]
cbc00a501b web: fix file upload form (cherry-pick #18808 to version-2025.12) (#18884)
Co-authored-by: Dominic R <dominic@sdko.org>
fix file upload form (#18808)
2025-12-17 14:02:31 +01:00
authentik-automation[bot]
480645d897 website/docs: add icon info to style guide (cherry-pick #18832 to version-2025.12) (#18837)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-17 14:02:12 +01:00
authentik-automation[bot]
997c767c95 web/admin: endpoint: change wording and add helper text (cherry-pick #18871 to version-2025.12) (#18890)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Teffen Ellis <teffen@sister.software>
2025-12-17 14:00:02 +01:00
authentik-automation[bot]
5a54e1dc9a web: fix notification counter (cherry-pick #18781 to version-2025.12) (#18882)
Co-authored-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>
fix notification counter (#18781)
2025-12-16 18:44:22 +01:00
authentik-automation[bot]
49b1952566 website/docs: Add docs for passkey autofill (WebauthN Conditional UI) (cherry-pick #18805 to version-2025.12) (#18870)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-12-16 18:11:02 +01:00
authentik-automation[bot]
e73edc2fce web/admin: fix read-only provider selection for application form (cherry-pick #18768 to version-2025.12) (#18803)
Co-authored-by: Dominic R <dominic@sdko.org>
fix read-only provider selection for application form (#18768)
2025-12-16 18:10:49 +01:00
authentik-automation[bot]
409652e874 web: add custom message with links for empty data export list (cherry-pick #18830 to version-2025.12) (#18876)
Co-authored-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>
2025-12-16 18:09:52 +01:00
authentik-automation[bot]
1d3fb6431f website/docs: 2025.10.3 release notes (cherry-pick #18868 to version-2025.12) (#18873)
website/docs: 2025.10.3 release notes (#18868)

* website/docs: 2025.10.3 release notes



* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-12-16 17:06:16 +01:00
authentik-automation[bot]
76cfada60f website/docs: adjust RBAC-related details in 2025.12 release notes (cherry-pick #18863 to version-2025.12) (#18869)
website/docs: adjust RBAC-related details in 2025.12 release notes (#18863)

* website/docs: adjust RBAC-related details in 2025.12 release notes

* adjust wording




---------

Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-12-16 10:39:50 -05:00
authentik-automation[bot]
ac45f80551 outposts: fix permission errors for related certificates (cherry-pick #18861 to version-2025.12) (#18866)
Co-authored-by: Jens L. <jens@goauthentik.io>
fix permission errors for related certificates (#18861)
2025-12-16 15:23:49 +01:00
authentik-automation[bot]
5ea85f086a web/admin/rbac: misc object permission fixes (cherry-pick #18859 to version-2025.12) (#18865)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
fixes (#18859)
2025-12-16 14:54:31 +01:00
authentik-automation[bot]
e3f657746c rbac: alter migrated direct permission roles (cherry-pick #18860 to version-2025.12) (#18864)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-12-16 13:56:35 +01:00
authentik-automation[bot]
001b56e2cc release: 2025.12.0-rc1 2025-12-16 04:59:24 +00:00
Marcelo Elizeche Landó
ecbfd2f0de add skip s3_test_server_available to TestResolveFileUrlS3Backend 2025-12-16 01:29:40 -03:00
authentik-automation[bot]
45753397e1 admin/files: fix get_objects_for_user queryset argument in FileUsedByView (cherry-pick #18845 to version-2025.12) (#18847)
admin/files: fix get_objects_for_user queryset argument in FileUsedByView (#18845)

Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-12-16 01:15:30 +00:00
Marcelo Elizeche Landó
dc6fe1dafe core: skip s3 tests if endpoint isn't available (#18841)
skip s3 tests if endpoint isn't available
2025-12-15 20:38:01 -03:00
authentik-automation[bot]
d5e8f2f416 admin/files: revert add check for /media existence (#18636) (cherry-pick #18829 to version-2025.12) (#18838)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-12-15 16:45:43 +01:00
authentik-automation[bot]
d73af5a2b4 packages/django-dramatiq-postgres: broker: close django connections on consumer close (cherry-pick #18833 to version-2025.12) (#18836)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Norman Ziebal <norman.ziebal@mail.schwarz>
close django connections on consumer close (#18833)
2025-12-15 15:01:08 +01:00
authentik-automation[bot]
7042f2bba8 core: list applications fix (cherry-pick #18798 to version-2025.12) (#18828)
Co-authored-by: Ryan Pesek <44002516+ryanpesek@users.noreply.github.com>
fix (#18798)
2025-12-15 12:47:20 +00:00
authentik-automation[bot]
efeb260fa8 tests/e2e: retry detached shadow roots (cherry-pick #18796 to version-2025.12) (#18799)
tests/e2e: retry detached shadow roots (#18796)

tests(e2e): retry detached shadow roots

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-12-14 00:50:23 +01:00
authentik-automation[bot]
29e90092ea website/release notes: Update v2025.12 release notes (cherry-pick #18797 to version-2025.12) (#18800)
website/release notes: Update v2025.12 release notes (#18797)

* website/release notes: Update v2025.12 release notes



* fix linting

---------

Signed-off-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-12-13 00:03:26 +00:00
Marcelo Elizeche Landó
0abe865023 Revert "Update docker compose command to start postgresql with s3"
This reverts commit 220c65a41a.
2025-12-12 20:59:44 -03:00
Marcelo Elizeche Landó
220c65a41a Update docker compose command to start postgresql with s3
Signed-off-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-12-12 19:47:24 -03:00
252 changed files with 4045 additions and 4899 deletions

View File

@@ -22,7 +22,7 @@ runs:
sudo rm -rf /usr/local/lib/android
- name: Install uv
if: ${{ contains(inputs.dependencies, 'python') }}
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v5
uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v5
with:
enable-cache: true
- name: Setup python
@@ -51,13 +51,13 @@ runs:
if: ${{ contains(inputs.dependencies, 'runtime') }}
uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7
with:
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
- name: Setup dependencies
if: ${{ contains(inputs.dependencies, 'runtime') }}
shell: bash
run: |
export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/compose.yml up -d
docker compose -f .github/actions/setup/docker-compose.yml up -d
cd web && npm i
- name: Generate config
if: ${{ contains(inputs.dependencies, 'python') }}

View File

@@ -11,6 +11,11 @@ services:
ports:
- 5432:5432
restart: always
redis:
image: docker.io/library/redis:7
ports:
- 6379:6379
restart: always
s3:
container_name: s3
image: docker.io/zenko/cloudserver

View File

@@ -12,11 +12,11 @@ runs:
with:
flags: ${{ inputs.flags }}
use_oidc: true
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1
with:
flags: ${{ inputs.flags }}
file: unittest.xml
use_oidc: true
report_type: test_results
- name: PostgreSQL Logs
shell: bash
run: |

View File

@@ -44,7 +44,7 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -85,7 +85,6 @@ jobs:
id: push
with:
context: .
file: lifecycle/container/Dockerfile
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
secrets: |
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
@@ -96,7 +95,7 @@ jobs:
platforms: linux/${{ inputs.image_arch }}
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames }}:buildcache-${{ inputs.image_arch }}
cache-to: ${{ steps.ev.outputs.cacheTo }}
- uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@@ -90,14 +90,14 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: int128/docker-manifest-create-action@6cdd53a8337cd50bc3ef8c7016579d8d460edd94 # v2
- uses: int128/docker-manifest-create-action@b60433fd4312d7a64a56d769b76ebe3f45cf36b4 # v2
id: build
with:
tags: ${{ matrix.tag }}
sources: |
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-amd64.outputs.image-digest }}
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-arm64.outputs.image-digest }}
- uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}

View File

@@ -41,7 +41,7 @@ jobs:
- working-directory: website/
name: Install Dependencies
run: npm ci
- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v4
- uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v4
with:
path: |
${{ github.workspace }}/website/api/.docusaurus
@@ -55,7 +55,7 @@ jobs:
env:
NODE_ENV: production
run: npm run build -w api
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4
with:
name: api-docs
path: website/api/build
@@ -67,7 +67,7 @@ jobs:
- build
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v5
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
with:
name: api-docs
path: website/api/build

View File

@@ -75,7 +75,7 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -101,7 +101,7 @@ jobs:
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache,mode=max' || '' }}
- uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@@ -24,5 +24,5 @@ jobs:
dir="/tmp/authentik/${{ matrix.version }}"
mkdir -p $dir
cd $dir
wget https://${{ matrix.version }}.goauthentik.io/compose.yml
wget https://${{ matrix.version }}.goauthentik.io/docker-compose.yml
${current}/scripts/test_docker.sh

View File

@@ -193,17 +193,15 @@ jobs:
glob: tests/e2e/test_source_scim*
- name: flows
glob: tests/e2e/test_flows*
- name: endpoints
glob: tests/e2e/test_endpoints_*
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Setup e2e env (chrome, etc)
run: |
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull
- id: cache-web
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v4
uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v4
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
@@ -223,54 +221,6 @@ jobs:
if: ${{ always() }}
with:
flags: e2e
test-openid-conformance:
name: test-openid-conformance (${{ matrix.job.name }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
job:
- name: basic
glob: tests/openid_conformance/test_basic.py
- name: implicit
glob: tests/openid_conformance/test_implicit.py
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Setup e2e env (chrome, etc)
run: |
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
- name: Setup conformance suite
run: |
docker compose -f tests/openid_conformance/compose.yml up -d --quiet-pull
- id: cache-web
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v4
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/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
run: |
npm ci
make -C .. gen-client-ts
npm run build
npm run build:sfe
- name: run conformance
run: |
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
flags: conformance
- if: ${{ !cancelled() }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: conformance-certification-${{ matrix.job.name }}
path: tests/openid_conformance/exports/
ci-core-mark:
if: always()
needs:

View File

@@ -92,7 +92,7 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -114,7 +114,7 @@ jobs:
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: lifecycle/container/${{ matrix.type }}.Dockerfile
file: ${{ matrix.type }}.Dockerfile
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
@@ -122,7 +122,7 @@ jobs:
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
- uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@@ -35,7 +35,7 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -58,7 +58,7 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
id: attest
if: true
with:
@@ -90,7 +90,7 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -121,10 +121,10 @@ jobs:
build-args: |
VERSION=${{ github.ref }}
tags: ${{ steps.ev.outputs.imageTags }}
file: lifecycle/container/${{ matrix.type }}.Dockerfile
file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
@@ -232,7 +232,7 @@ jobs:
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
docker cp ${container}:web/ .
- name: Create a Sentry.io release
uses: getsentry/action-release@dab6548b3c03c4717878099e43782cf5be654289 # v3
uses: getsentry/action-release@128c5058bbbe93c8e02147fe0a9c713f166259a6 # v3
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

3
.gitignore vendored
View File

@@ -211,5 +211,4 @@ source_docs/
/vendor/
### Docker ###
tests/openid_conformance/exports/*.zip
compose.override.yml
docker-compose.override.yml

View File

@@ -16,8 +16,10 @@ go.sum @goauthentik/backend
# Infrastructure
.github/ @goauthentik/infrastructure
lifecycle/aws/ @goauthentik/infrastructure
lifecycle/container/ @goauthentik/infrastructure
Dockerfile @goauthentik/infrastructure
*Dockerfile @goauthentik/infrastructure
.dockerignore @goauthentik/infrastructure
docker-compose.yml @goauthentik/infrastructure
Makefile @goauthentik/infrastructure
.editorconfig @goauthentik/infrastructure
CODEOWNERS @goauthentik/infrastructure

View File

@@ -26,7 +26,7 @@ RUN npm run build && \
npm run build:sfe
# Stage 2: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS go-builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS go-builder
ARG TARGETOS
ARG TARGETARCH
@@ -76,7 +76,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 4: Download uv
FROM ghcr.io/astral-sh/uv:0.9.18@sha256:5713fa8217f92b80223bc83aac7db36ec80a84437dbc0d04bbc659cae030d8c9 AS uv
FROM ghcr.io/astral-sh/uv:0.9.17@sha256:5cb6b54d2bc3fe2eb9a8483db958a0b9eebf9edff68adedb369df8e7b98711a2 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff AS python-base

View File

@@ -9,13 +9,6 @@ NPM_VERSION = $(shell python -m scripts.generate_semver)
PY_SOURCES = authentik packages tests scripts lifecycle .github
DOCKER_IMAGE ?= "authentik:test"
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
SED_INPLACE = sed -i ''
else
SED_INPLACE = sed -i
endif
GEN_API_TS = gen-ts-api
GEN_API_PY = gen-py-api
GEN_API_GO = gen-go-api
@@ -126,8 +119,8 @@ bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
ifndef version
$(error Usage: make bump version=20xx.xx.xx )
endif
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' pyproject.toml
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
sed -i 's/^version = ".*"/version = "$(version)"/' pyproject.toml
sed -i 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
$(MAKE) gen-build gen-compose aws-cfn
npm version --no-git-tag-version --allow-same-version $(version)
cd ${PWD}/web && npm version --no-git-tag-version --allow-same-version $(version)
@@ -141,10 +134,14 @@ gen-build: ## Extract the schema from the database
AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
uv run ak build_schema
uv run ak make_blueprint_schema --file blueprints/schema.json
AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
uv run ak spectacular --file schema.yml
gen-compose:
uv run scripts/generate_compose.py
uv run scripts/generate_docker_compose.py
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
@@ -152,14 +149,14 @@ gen-changelog: ## (Release) generate the changelog based from the commits since
gen-diff: ## (Release) generate the changelog diff between the current schema and the last tag
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > schema-old.yml
docker compose -f scripts/api/compose.yml run --rm --user "${UID}:${GID}" diff \
docker compose -f scripts/api/docker-compose.yml run --rm --user "${UID}:${GID}" diff \
--markdown \
/local/diff.md \
/local/schema-old.yml \
/local/schema.yml
rm schema-old.yml
$(SED_INPLACE) 's/{/&#123;/g' diff.md
$(SED_INPLACE) 's/}/&#125;/g' diff.md
sed -i 's/{/&#123;/g' diff.md
sed -i 's/}/&#125;/g' diff.md
npx prettier --write diff.md
gen-clean-ts: ## Remove generated API client for TypeScript
@@ -175,7 +172,7 @@ gen-clean-go: ## Remove generated API client for Go
gen-clean: gen-clean-ts gen-clean-go gen-clean-py ## Remove generated API clients
gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescript into the authentik UI Application
docker compose -f scripts/api/compose.yml run --rm --user "${UID}:${GID}" gen \
docker compose -f scripts/api/docker-compose.yml run --rm --user "${UID}:${GID}" gen \
generate \
-i /local/schema.yml \
-g typescript-fetch \
@@ -296,7 +293,7 @@ docs-api-clean: ## Clean generated API documentation
docker: ## Build a docker image of the current source tree
mkdir -p ${GEN_API_TS}
DOCKER_BUILDKIT=1 docker build . -f lifecycle/container/Dockerfile --progress plain --tag ${DOCKER_IMAGE}
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
test-docker:
BUILD=true ${PWD}/scripts/test_docker.sh

View File

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2026.2.0-rc1"
VERSION = "2025.12.0-rc3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -62,10 +62,10 @@ class TestSanitizeFilePath(TestCase):
"test@file.png", # @
"test#file.png", # #
"test$file.png", # $
"test%file.png", # %
"test%file.png", # % (but %(theme)s is allowed)
"test&file.png", # &
"test*file.png", # *
"test(file).png", # parentheses
"test(file).png", # parentheses (but %(theme)s is allowed)
"test[file].png", # brackets
"test{file}.png", # braces
]
@@ -108,3 +108,30 @@ class TestSanitizeFilePath(TestCase):
with self.assertRaises(ValidationError):
validate_file_name(path)
def test_sanitize_theme_variable_valid(self):
"""Test sanitizing filename with %(theme)s variable"""
# These should all be valid
validate_file_name("logo-%(theme)s.png")
validate_file_name("brand/logo-%(theme)s.svg")
validate_file_name("images/icon-%(theme)s.png")
validate_file_name("%(theme)s/logo.png")
validate_file_name("brand/%(theme)s/logo.png")
def test_sanitize_theme_variable_multiple(self):
"""Test sanitizing filename with multiple %(theme)s variables"""
validate_file_name("%(theme)s/logo-%(theme)s.png")
def test_sanitize_theme_variable_invalid_format(self):
"""Test that partial or malformed theme variables are rejected"""
invalid_paths = [
"test%(theme.png", # missing )s
"test%theme)s.png", # missing (
"test%(themes).png", # wrong variable name
"test%(THEME)s.png", # wrong case
"test%()s.png", # empty variable name
]
for path in invalid_paths:
with self.assertRaises(ValidationError):
validate_file_name(path)

View File

@@ -12,6 +12,10 @@ from authentik.admin.files.usage import FileUsage
MAX_FILE_NAME_LENGTH = 1024
MAX_PATH_COMPONENT_LENGTH = 255
# Theme variable placeholder that can be used in file paths
# This allows for theme-specific files like logo-%(theme)s.png
THEME_VARIABLE = "%(theme)s"
def validate_file_name(name: str) -> None:
if PassthroughBackend(FileUsage.MEDIA).supports_file(name) or StaticBackend(
@@ -39,12 +43,17 @@ def validate_upload_file_name(
if not name:
raise ValidationError(_("File name cannot be empty"))
# Same regex is used in the frontend as well
if not re.match(r"^[a-zA-Z0-9._/-]+$", name):
# Allow %(theme)s placeholder for theme-specific files
# We temporarily replace it for validation, then check the result
name_for_validation = name.replace(THEME_VARIABLE, "theme")
# Same regex is used in the frontend as well (without %(theme)s handling there)
if not re.match(r"^[a-zA-Z0-9._/-]+$", name_for_validation):
raise ValidationError(
_(
"File name can only contain letters (a-z, A-Z), numbers (0-9), "
"dots (.), hyphens (-), underscores (_), and forward slashes (/)"
"dots (.), hyphens (-), underscores (_), forward slashes (/), "
"and the special placeholder %(theme)s for theme-specific files"
)
)

View File

@@ -1,45 +0,0 @@
from json import dumps
from django.core.management.base import BaseCommand, no_translations
from drf_spectacular.drainage import GENERATOR_STATS
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.renderers import OpenApiYamlRenderer
from drf_spectacular.validation import validate_schema
from structlog.stdlib import get_logger
from authentik.blueprints.v1.schema import SchemaBuilder
class Command(BaseCommand):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.logger = get_logger()
def add_arguments(self, parser):
parser.add_argument("--blueprint-file", type=str, default="blueprints/schema.json")
parser.add_argument("--api-file", type=str, default="schema.yml")
@no_translations
def handle(self, *args, blueprint_file: str, api_file: str, **options):
self.build_blueprint(blueprint_file)
self.build_api(api_file)
def build_blueprint(self, file: str):
self.logger.debug("Building blueprint schema...", file=file)
blueprint_builder = SchemaBuilder()
blueprint_builder.build()
with open(file, "w") as _schema:
_schema.write(
dumps(blueprint_builder.schema, indent=4, default=SchemaBuilder.json_default)
)
def build_api(self, file: str):
self.logger.debug("Building API schema...", file=file)
generator = SchemaGenerator()
schema = generator.get_schema(request=None, public=True)
GENERATOR_STATS.emit_summary()
validate_schema(schema)
output = OpenApiYamlRenderer().render(schema, renderer_context={})
with open(file, "wb") as f:
f.write(output)

View File

@@ -1,14 +1,9 @@
"""Schema generation tests"""
from pathlib import Path
from django.core.management import call_command
from django.urls import reverse
from rest_framework.test import APITestCase
from yaml import safe_load
from authentik.lib.config import CONFIG
class TestSchemaGeneration(APITestCase):
"""Generic admin tests"""
@@ -26,18 +21,3 @@ class TestSchemaGeneration(APITestCase):
reverse("authentik_api:schema-browser"),
)
self.assertEqual(response.status_code, 200)
def test_build_schema(self):
"""Test schema build command"""
blueprint_file = Path("blueprints/schema.json")
api_file = Path("schema.yml")
blueprint_file.unlink()
api_file.unlink()
with (
CONFIG.patch("debug", True),
CONFIG.patch("tenants.enabled", True),
CONFIG.patch("outposts.disable_embedded_outpost", True),
):
call_command("build_schema")
self.assertTrue(blueprint_file.exists())
self.assertTrue(api_file.exists())

View File

@@ -1,7 +1,9 @@
"""Generate JSON Schema for blueprints"""
from json import dumps
from typing import Any
from django.core.management.base import BaseCommand, no_translations
from django.db.models import Model, fields
from django.db.models.fields.related import OneToOneField
from drf_jsonschema_serializer.convert import converter, field_to_converter
@@ -38,12 +40,13 @@ class PrimaryKeyRelatedFieldConverter:
return {"type": "integer"}
class SchemaBuilder:
class Command(BaseCommand):
"""Generate JSON Schema for blueprints"""
schema: dict
def __init__(self):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.schema = {
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
@@ -90,6 +93,16 @@ class SchemaBuilder:
"$defs": {"blueprint_entry": {"oneOf": []}},
}
def add_arguments(self, parser):
parser.add_argument("--file", type=str)
@no_translations
def handle(self, *args, file: str, **options):
"""Generate JSON Schema for blueprints"""
self.build()
with open(file, "w") as _schema:
_schema.write(dumps(self.schema, indent=4, default=Command.json_default))
@staticmethod
def json_default(value: Any) -> Any:
"""Helper that handles gettext_lazy strings that JSON doesn't handle"""
@@ -111,7 +124,7 @@ class SchemaBuilder:
try:
serializer_class = model_instance.serializer
except NotImplementedError as exc:
raise ValueError(f"SerializerModel not implemented by {model}") from exc
raise NotImplementedError(model_instance) from exc
serializer = serializer_class(
context={
SERIALIZER_CONTEXT_BLUEPRINT: False,

View File

@@ -15,6 +15,7 @@ from django.db.models import Model
from django.db.models.query_utils import Q
from django.db.transaction import atomic
from django.db.utils import IntegrityError
from django_channels_postgres.models import GroupChannel, Message
from guardian.models import RoleObjectPermission, UserObjectPermission
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer, Serializer
@@ -40,17 +41,55 @@ from authentik.core.models import (
User,
UserSourceConnection,
)
from authentik.endpoints.models import Connector
from authentik.endpoints.connectors.agent.models import (
AgentDeviceConnection,
AppleNonce,
DeviceAuthenticationToken,
)
from authentik.endpoints.connectors.agent.models import (
DeviceToken as EndpointDeviceToken,
)
from authentik.endpoints.models import Connector, Device, DeviceConnection, DeviceFactSnapshot
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import LicenseUsage
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceProviderGroup,
GoogleWorkspaceProviderUser,
)
from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProviderGroup,
MicrosoftEntraProviderUser,
)
from authentik.enterprise.providers.ssf.models import StreamEvent
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
EndpointDevice,
EndpointDeviceConnection,
)
from authentik.events.logs import LogEvent, capture_logs
from authentik.events.utils import cleanse_dict
from authentik.flows.models import Stage
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.flows.models import FlowToken, Stage
from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.reflection import get_apps
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import (
AccessToken,
AuthorizationCode,
DeviceToken,
RefreshToken,
)
from authentik.providers.proxy.models import ProxySession
from authentik.providers.rac.models import ConnectionToken
from authentik.providers.saml.models import SAMLSession
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.rbac.models import Role
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
from authentik.stages.consent.models import UserConsent
from authentik.tasks.models import Task, TaskLog
from authentik.tenants.models import Tenant
# Context set when the serializer is created in a blueprint context
# Update website/docs/customize/blueprints/v1/models.md when used
@@ -86,16 +125,49 @@ def excluded_models() -> list[type[Model]]:
# Classes that have other dependencies
Session,
AuthenticatedSession,
# Classes which are only internally managed
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
FlowToken,
LicenseUsage,
SCIMProviderGroup,
SCIMProviderUser,
Tenant,
Task,
TaskLog,
ConnectionToken,
AuthorizationCode,
AccessToken,
RefreshToken,
ProxySession,
Reputation,
WebAuthnDeviceType,
SCIMSourceUser,
SCIMSourceGroup,
GoogleWorkspaceProviderUser,
GoogleWorkspaceProviderGroup,
MicrosoftEntraProviderUser,
MicrosoftEntraProviderGroup,
EndpointDevice,
EndpointDeviceConnection,
EndpointDeviceToken,
Device,
DeviceConnection,
DeviceAuthenticationToken,
AppleNonce,
AgentDeviceConnection,
DeviceFactSnapshot,
DeviceToken,
StreamEvent,
UserConsent,
SAMLSession,
Message,
GroupChannel,
)
def is_model_allowed(model: type[Model]) -> bool:
"""Check if model is allowed"""
return (
model not in excluded_models()
and issubclass(model, SerializerModel | BaseMetaModel)
and not issubclass(model, InternallyManagedMixin)
)
return model not in excluded_models() and issubclass(model, SerializerModel | BaseMetaModel)
class DoRollback(SentryIgnoredException):

View File

@@ -85,6 +85,7 @@ class GroupSerializer(ModelSerializer):
source="roles",
required=False,
)
inherited_roles_obj = SerializerMethodField(allow_null=True)
num_pk = IntegerField(read_only=True)
@property
@@ -108,6 +109,13 @@ class GroupSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_parents", "false")).lower() == "true"
@property
def _should_include_inherited_roles(self) -> bool:
request: Request = self.context.get("request", None)
if not request:
return True
return str(request.query_params.get("include_inherited_roles", "false")).lower() == "true"
@extend_schema_field(PartialUserSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[PartialUserSerializer] | None:
if not self._should_include_users:
@@ -126,6 +134,15 @@ class GroupSerializer(ModelSerializer):
return None
return RelatedGroupSerializer(instance.parents, many=True).data
@extend_schema_field(RoleSerializer(many=True))
def get_inherited_roles_obj(self, instance: Group) -> list | None:
"""Return only inherited roles from ancestor groups (excludes direct roles)"""
if not self._should_include_inherited_roles:
return None
direct_role_pks = instance.roles.values_list("pk", flat=True)
inherited_roles = instance.all_roles().exclude(pk__in=direct_role_pks)
return RoleSerializer(inherited_roles, many=True).data
def validate_is_superuser(self, superuser: bool):
"""Ensure that the user creating this group has permissions to set the superuser flag"""
request: Request = self.context.get("request", None)
@@ -167,6 +184,7 @@ class GroupSerializer(ModelSerializer):
"attributes",
"roles",
"roles_obj",
"inherited_roles_obj",
"children",
"children_obj",
]
@@ -289,6 +307,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_users", bool, default=True),
OpenApiParameter("include_children", bool, default=False),
OpenApiParameter("include_parents", bool, default=False),
OpenApiParameter("include_inherited_roles", bool, default=False),
]
)
def list(self, request, *args, **kwargs):
@@ -299,6 +318,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_users", bool, default=True),
OpenApiParameter("include_children", bool, default=False),
OpenApiParameter("include_parents", bool, default=False),
OpenApiParameter("include_inherited_roles", bool, default=False),
]
)
def retrieve(self, request, *args, **kwargs):

View File

@@ -4,6 +4,7 @@ from typing import Any
from django.utils.timezone import now
from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.shortcuts import get_anonymous_user
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField
@@ -144,6 +145,12 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
owner_field = "user"
rbac_allow_create_without_perm = True
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)
def perform_create(self, serializer: TokenSerializer):
if not self.request.user.is_superuser:
instance = serializer.save(

View File

@@ -5,10 +5,9 @@ from django.test import TestCase
from authentik.core.models import Group, PropertyMapping, Source, User
from authentik.core.sources.mapper import SourceMapper
from authentik.lib.generators import generate_id
from authentik.lib.models import InternallyManagedMixin
class ProxySource(InternallyManagedMixin, Source):
class ProxySource(Source):
@property
def property_mapping_type(self):
return PropertyMapping

View File

@@ -183,16 +183,16 @@ class TestTokenAPI(APITestCase):
self.assertEqual(len(body["results"]), 1)
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
def test_list_with_permission(self):
"""Test Token List (Test with `view_token` permission)"""
def test_list_admin(self):
"""Test Token List (Test with admin auth)"""
Token.objects.all().delete()
self.client.force_login(self.admin)
token_should: Token = Token.objects.create(
identifier="test", expiring=False, user=self.user
)
token_should_not: Token = Token.objects.create(
identifier="test-2", expiring=False, user=get_anonymous_user()
)
self.user.assign_perms_to_managed_role("authentik_core.view_token")
response = self.client.get(reverse("authentik_api:token-list"))
body = loads(response.content)
self.assertEqual(len(body["results"]), 2)

View File

@@ -1,5 +1,7 @@
"""Crypto API Views"""
from datetime import datetime
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate
@@ -13,12 +15,14 @@ from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
extend_schema,
extend_schema_field,
)
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
CharField,
ChoiceField,
DateTimeField,
IntegerField,
SerializerMethodField,
)
@@ -47,15 +51,59 @@ LOGGER = get_logger()
class CertificateKeyPairSerializer(ModelSerializer):
"""CertificateKeyPair Serializer"""
fingerprint_sha256 = SerializerMethodField()
fingerprint_sha1 = SerializerMethodField()
cert_expiry = SerializerMethodField()
cert_subject = SerializerMethodField()
private_key_available = SerializerMethodField()
key_type = SerializerMethodField()
certificate_download_url = SerializerMethodField()
private_key_download_url = SerializerMethodField()
@property
def _should_include_details(self) -> bool:
request: Request = self.context.get("request", None)
if not request:
return True
return str(request.query_params.get("include_details", "true")).lower() == "true"
def get_fingerprint_sha256(self, instance: CertificateKeyPair) -> str | None:
"Get certificate Hash (SHA256)"
if not self._should_include_details:
return None
return instance.fingerprint_sha256
def get_fingerprint_sha1(self, instance: CertificateKeyPair) -> str | None:
"Get certificate Hash (SHA1)"
if not self._should_include_details:
return None
return instance.fingerprint_sha1
def get_cert_expiry(self, instance: CertificateKeyPair) -> datetime | None:
"Get certificate expiry"
if not self._should_include_details:
return None
return DateTimeField().to_representation(instance.certificate.not_valid_after_utc)
def get_cert_subject(self, instance: CertificateKeyPair) -> str | None:
"""Get certificate subject as full rfc4514"""
if not self._should_include_details:
return None
return instance.certificate.subject.rfc4514_string()
def get_private_key_available(self, instance: CertificateKeyPair) -> bool:
"""Show if this keypair has a private key configured or not"""
return instance.key_data != "" and instance.key_data is not None
@extend_schema_field(ChoiceField(choices=KeyType.choices, allow_null=True))
def get_key_type(self, instance: CertificateKeyPair) -> str | None:
"""Get the key algorithm type from the certificate's public key"""
if not self._should_include_details:
return None
return instance.key_type
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
"""Get URL to download certificate"""
return (
@@ -127,11 +175,6 @@ class CertificateKeyPairSerializer(ModelSerializer):
"managed": {"read_only": True},
"key_data": {"write_only": True},
"certificate_data": {"write_only": True},
"fingerprint_sha256": {"read_only": True},
"fingerprint_sha1": {"read_only": True},
"cert_expiry": {"read_only": True},
"cert_subject": {"read_only": True},
"key_type": {"read_only": True},
}
@@ -173,12 +216,17 @@ class CertificateKeyPairFilter(FilterSet):
return queryset.exclude(key_data__exact="")
def filter_key_type(self, queryset, name, value): # pragma: no cover
"""Filter certificates by key type using the stored database field"""
"""Filter certificates by key type using the public key from the certificate"""
if not value:
return queryset
# value is a list of KeyType enum values from MultipleChoiceFilter
return queryset.filter(key_type__in=value)
filtered_pks = []
for cert in queryset:
if cert.key_type in value:
filtered_pks.append(cert.pk)
return queryset.filter(pk__in=filtered_pks)
class Meta:
model = CertificateKeyPair
@@ -215,6 +263,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
"Can be specified multiple times (e.g. '?key_type=rsa&key_type=ec')"
),
),
OpenApiParameter("include_details", bool, default=True),
]
)
def list(self, request, *args, **kwargs):

View File

@@ -1,120 +0,0 @@
# Generated by Django 5.2.9 on 2025-12-09 06:22
from hashlib import md5
from cryptography.hazmat.backends import default_backend
from cryptography.x509 import load_pem_x509_certificate
from django.db import migrations, models
from authentik.crypto.signals import extract_certificate_metadata
from authentik.lib.migrations import progress_bar
def backfill_certificate_metadata(apps, schema_editor): # noqa: ARG001
"""Backfill certificate metadata and kid for existing records."""
db_alias = schema_editor.connection.alias
CertificateKeyPair = apps.get_model("authentik_crypto", "CertificateKeyPair")
print("\nStoring extra data about certificates, this might take a couple of minutes...")
for cert in progress_bar(CertificateKeyPair.objects.using(db_alias).all()):
updated_fields = []
if cert.certificate_data:
try:
certificate = load_pem_x509_certificate(
cert.certificate_data.encode("utf-8"), default_backend()
)
metadata = extract_certificate_metadata(certificate)
cert.key_type = metadata["key_type"]
cert.cert_expiry = metadata["cert_expiry"]
cert.cert_subject = metadata["cert_subject"]
cert.fingerprint_sha256 = metadata["fingerprint_sha256"]
cert.fingerprint_sha1 = metadata["fingerprint_sha1"]
updated_fields.extend(
[
"key_type",
"cert_expiry",
"cert_subject",
"fingerprint_sha256",
"fingerprint_sha1",
]
)
except (ValueError, TypeError, AttributeError):
pass
# Backfill kid with MD5 for backwards compatibility
if cert.key_data:
cert.kid = md5(cert.key_data.encode("utf-8"), usedforsecurity=False).hexdigest()
updated_fields.append("kid")
if updated_fields:
cert.save(update_fields=updated_fields, using=db_alias)
class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0005_alter_certificatekeypair_options"),
]
operations = [
migrations.AddField(
model_name="certificatekeypair",
name="cert_expiry",
field=models.DateTimeField(blank=True, help_text="Certificate expiry date", null=True),
),
migrations.AddField(
model_name="certificatekeypair",
name="cert_subject",
field=models.TextField(
blank=True, help_text="Certificate subject as RFC4514 string", null=True
),
),
migrations.AddField(
model_name="certificatekeypair",
name="fingerprint_sha1",
field=models.CharField(
blank=True,
help_text="SHA1 fingerprint of the certificate",
max_length=59,
null=True,
),
),
migrations.AddField(
model_name="certificatekeypair",
name="fingerprint_sha256",
field=models.CharField(
blank=True,
help_text="SHA256 fingerprint of the certificate",
max_length=95,
null=True,
),
),
migrations.AddField(
model_name="certificatekeypair",
name="key_type",
field=models.CharField(
blank=True,
choices=[
("rsa", "RSA"),
("ec", "Elliptic Curve"),
("dsa", "DSA"),
("ed25519", "Ed25519"),
("ed448", "Ed448"),
],
help_text="Key algorithm type detected from the certificate's public key",
max_length=16,
null=True,
),
),
migrations.AddField(
model_name="certificatekeypair",
name="kid",
field=models.CharField(
blank=True, help_text="Key ID generated from private key", max_length=128, null=True
),
),
migrations.RunPython(backfill_certificate_metadata, migrations.RunPython.noop),
]

View File

@@ -1,8 +1,7 @@
"""authentik crypto models"""
from base64 import urlsafe_b64encode
from binascii import hexlify
from hashlib import md5, sha512
from hashlib import md5
from ssl import PEM_FOOTER, PEM_HEADER
from textwrap import wrap
from uuid import uuid4
@@ -48,39 +47,6 @@ def fingerprint_sha256(cert: Certificate) -> str:
return hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8")
def detect_key_type(certificate: Certificate) -> str | None:
"""Detect the key algorithm type by parsing the certificate's public key"""
try:
public_key = certificate.public_key()
if isinstance(public_key, RSAPublicKey):
return KeyType.RSA
if isinstance(public_key, EllipticCurvePublicKey):
return KeyType.EC
if isinstance(public_key, DSAPublicKey):
return KeyType.DSA
if isinstance(public_key, Ed25519PublicKey):
return KeyType.ED25519
if isinstance(public_key, Ed448PublicKey):
return KeyType.ED448
except (ValueError, TypeError, AttributeError) as exc:
LOGGER.warning("Failed to detect key type", exc=exc)
return None
def generate_key_id(key_data: str) -> str:
"""Generate Key ID using SHA512 + urlsafe_b64encode."""
if not key_data:
return ""
return urlsafe_b64encode(sha512(key_data.encode("utf-8")).digest()).decode("utf-8").rstrip("=")
def generate_key_id_legacy(key_data: str) -> str:
"""Generate Key ID using MD5 (legacy format for backwards compatibility)."""
if not key_data:
return ""
return md5(key_data.encode("utf-8")).hexdigest() # nosec
class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
is set, otherwise it can be used to verify remote data."""
@@ -96,41 +62,6 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
blank=True,
default="",
)
key_type = models.CharField(
max_length=16,
choices=KeyType.choices,
null=True,
blank=True,
help_text=_("Key algorithm type detected from the certificate's public key"),
)
cert_expiry = models.DateTimeField(
null=True,
blank=True,
help_text=_("Certificate expiry date"),
)
cert_subject = models.TextField(
null=True,
blank=True,
help_text=_("Certificate subject as RFC4514 string"),
)
fingerprint_sha256 = models.CharField(
max_length=95,
null=True,
blank=True,
help_text=_("SHA256 fingerprint of the certificate"),
)
fingerprint_sha1 = models.CharField(
max_length=59,
null=True,
blank=True,
help_text=_("SHA1 fingerprint of the certificate"),
)
kid = models.CharField(
max_length=128,
null=True,
blank=True,
help_text=_("Key ID generated from private key"),
)
_cert: Certificate | None = None
_private_key: PrivateKeyTypes | None = None
@@ -175,6 +106,41 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
return None
return self._private_key
@property
def fingerprint_sha256(self) -> str:
"""Get SHA256 Fingerprint of certificate_data"""
return fingerprint_sha256(self.certificate)
@property
def fingerprint_sha1(self) -> str:
"""Get SHA1 Fingerprint of certificate_data"""
return hexlify(self.certificate.fingerprint(hashes.SHA1()), ":").decode("utf-8") # nosec
@property
def kid(self):
"""Get Key ID used for JWKS"""
return (
md5(self.key_data.encode("utf-8"), usedforsecurity=False).hexdigest()
if self.key_data
else ""
) # nosec
@property
def key_type(self) -> str | None:
"""Get the key algorithm type from the certificate's public key"""
public_key = self.certificate.public_key()
if isinstance(public_key, RSAPublicKey):
return KeyType.RSA
if isinstance(public_key, EllipticCurvePublicKey):
return KeyType.EC
if isinstance(public_key, DSAPublicKey):
return KeyType.DSA
if isinstance(public_key, Ed25519PublicKey):
return KeyType.ED25519
if isinstance(public_key, Ed448PublicKey):
return KeyType.ED448
return None
def __str__(self) -> str:
return f"Certificate-Key Pair {self.name}"

View File

@@ -1,70 +0,0 @@
"""authentik crypto signals"""
from binascii import hexlify
from datetime import datetime
from ssl import CertificateError
from cryptography.hazmat.primitives import hashes
from cryptography.x509 import Certificate
from django.db.models.signals import pre_save
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.crypto.models import (
CertificateKeyPair,
detect_key_type,
fingerprint_sha256,
generate_key_id,
generate_key_id_legacy,
)
LOGGER = get_logger()
def extract_certificate_metadata(certificate: Certificate) -> dict[str, str | datetime]:
"""Extract all metadata fields from a certificate."""
metadata = {}
try:
metadata["key_type"] = detect_key_type(certificate)
metadata["cert_expiry"] = certificate.not_valid_after_utc
metadata["cert_subject"] = certificate.subject.rfc4514_string()
metadata["fingerprint_sha256"] = fingerprint_sha256(certificate)
metadata["fingerprint_sha1"] = hexlify(
certificate.fingerprint(hashes.SHA1()), ":" # nosec
).decode("utf-8")
except (ValueError, TypeError, AttributeError) as exc:
raise CertificateError(f"Invalid certificate metadata: {exc}") from exc
return metadata
@receiver(pre_save, sender="authentik_crypto.CertificateKeyPair")
def certificate_key_pair_pre_save(
sender: type[CertificateKeyPair], instance: CertificateKeyPair, **_
):
"""Automatically populate certificate metadata fields before saving"""
# Only extract metadata if certificate_data is present
if not instance.certificate_data:
return
try:
metadata = extract_certificate_metadata(instance.certificate)
except (CertificateError, ValueError, TypeError, AttributeError) as exc:
LOGGER.warning("Failed to extract certificate metadata", exc=exc)
return
instance.key_type = metadata["key_type"]
instance.cert_expiry = metadata["cert_expiry"]
instance.cert_subject = metadata["cert_subject"]
instance.fingerprint_sha256 = metadata["fingerprint_sha256"]
instance.fingerprint_sha1 = metadata["fingerprint_sha1"]
# Generate kid if not set, or regenerate if key_data has changed
# Preserve existing kid (MD5 or SHA512) if it matches the current key_data
if instance.key_data:
new_kid = generate_key_id(instance.key_data)
legacy_kid = generate_key_id_legacy(instance.key_data)
if instance.kid not in (new_kid, legacy_kid):
instance.kid = new_kid

View File

@@ -20,7 +20,7 @@ from authentik.core.tests.utils import (
)
from authentik.crypto.api import CertificateKeyPairSerializer
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair, generate_key_id, generate_key_id_legacy
from authentik.crypto.models import CertificateKeyPair
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id, generate_key
@@ -173,24 +173,21 @@ class TestCrypto(APITestCase):
self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1)
self.assertEqual(api_cert["fingerprint_sha256"], cert.fingerprint_sha256)
def test_list_always_includes_details(self):
"""Test API List always includes certificate details"""
def test_list_without_details(self):
"""Test API List (no details)"""
cert = create_test_cert()
self.client.force_login(create_test_admin_user())
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-list",
),
data={"name": cert.name},
data={"name": cert.name, "include_details": False},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
api_cert = [x for x in body["results"] if x["name"] == cert.name][0]
# All details should now always be included
self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1)
self.assertEqual(api_cert["fingerprint_sha256"], cert.fingerprint_sha256)
self.assertIsNotNone(api_cert["cert_expiry"])
self.assertIsNotNone(api_cert["cert_subject"])
self.assertEqual(api_cert["fingerprint_sha1"], None)
self.assertEqual(api_cert["fingerprint_sha256"], None)
def test_certificate_download(self):
"""Test certificate export (download)"""
@@ -429,114 +426,3 @@ class TestCrypto(APITestCase):
self.assertEqual(
1, final_count, "Should not create duplicate cert for same private key"
)
def test_metadata_extraction_with_cert_and_key(self):
"""Test that metadata is extracted when creating keypair with certificate and key"""
cert = create_test_cert()
# Verify all metadata fields are populated
self.assertIsNotNone(cert.key_type)
self.assertIsNotNone(cert.cert_expiry)
self.assertIsNotNone(cert.cert_subject)
self.assertIsNotNone(cert.fingerprint_sha256)
self.assertIsNotNone(cert.fingerprint_sha1)
# Verify kid is generated using SHA512 for new records
self.assertIsNotNone(cert.kid)
self.assertEqual(cert.kid, generate_key_id(cert.key_data))
def test_metadata_extraction_without_key(self):
"""Test that metadata is extracted when creating keypair without private key"""
builder = CertificateBuilder(generate_id())
builder.build(subject_alt_names=[], validity_days=3)
# Create keypair with only certificate, no key
cert = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=builder.certificate,
key_data="",
)
# Verify certificate metadata fields are populated
self.assertIsNotNone(cert.key_type)
self.assertIsNotNone(cert.cert_expiry)
self.assertIsNotNone(cert.cert_subject)
self.assertIsNotNone(cert.fingerprint_sha256)
self.assertIsNotNone(cert.fingerprint_sha1)
# Verify kid is empty when no key_data
self.assertEqual(cert.kid, None)
def test_metadata_extraction_invalid_cert(self):
"""Test that invalid certificate data doesn't crash, just skips metadata"""
cert = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data="invalid certificate data",
key_data="",
)
# Verify metadata fields are None for invalid cert
self.assertIsNone(cert.key_type)
self.assertIsNone(cert.cert_expiry)
self.assertIsNone(cert.cert_subject)
self.assertIsNone(cert.fingerprint_sha256)
self.assertIsNone(cert.fingerprint_sha1)
self.assertIsNone(cert.kid)
def test_kid_legacy_preservation(self):
"""Test that legacy MD5 kid is preserved when key_data hasn't changed"""
cert = create_test_cert()
# Simulate a legacy MD5 kid (as if backfilled from old system)
legacy_kid = generate_key_id_legacy(cert.key_data)
CertificateKeyPair.objects.filter(pk=cert.pk).update(kid=legacy_kid)
cert.refresh_from_db()
self.assertEqual(cert.kid, legacy_kid)
# Save the cert again (e.g., name change) - kid should be preserved
cert.name = generate_id()
cert.save()
cert.refresh_from_db()
self.assertEqual(cert.kid, legacy_kid)
def test_kid_regenerated_on_key_change(self):
"""Test that kid is regenerated when key_data changes"""
cert = create_test_cert()
original_kid = cert.kid
# Generate a new key and update the keypair
builder = CertificateBuilder(generate_id())
builder.build(subject_alt_names=[], validity_days=3)
cert.key_data = builder.private_key
cert.certificate_data = builder.certificate
cert.save()
cert.refresh_from_db()
# Kid should be regenerated for the new key
self.assertNotEqual(cert.kid, original_kid)
self.assertEqual(cert.kid, generate_key_id(cert.key_data))
def test_kid_regenerated_on_key_change_from_legacy(self):
"""Test that kid is regenerated from legacy MD5 when key_data changes"""
cert = create_test_cert()
# Simulate a legacy MD5 kid
legacy_kid = generate_key_id_legacy(cert.key_data)
CertificateKeyPair.objects.filter(pk=cert.pk).update(kid=legacy_kid)
cert.refresh_from_db()
self.assertEqual(cert.kid, legacy_kid)
# Generate a new key and update the keypair
builder = CertificateBuilder(generate_id())
builder.build(subject_alt_names=[], validity_days=3)
cert.key_data = builder.private_key
cert.certificate_data = builder.certificate
cert.save()
cert.refresh_from_db()
# Kid should now be SHA512 for the new key
self.assertNotEqual(cert.kid, legacy_kid)
self.assertEqual(cert.kid, generate_key_id(cert.key_data))

View File

@@ -16,7 +16,7 @@ from authentik.endpoints.models import (
)
from authentik.flows.stage import StageView
from authentik.lib.generators import generate_key
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
if TYPE_CHECKING:
@@ -97,7 +97,7 @@ class AgentDeviceUserBinding(DeviceUserBinding):
apple_enclave_key_id = models.TextField()
class DeviceToken(InternallyManagedMixin, ExpiringModel):
class DeviceToken(ExpiringModel):
"""Per-device token used for authentication."""
token_uuid = models.UUIDField(primary_key=True, default=uuid4)
@@ -143,7 +143,7 @@ class EnrollmentToken(ExpiringModel, SerializerModel):
]
class DeviceAuthenticationToken(InternallyManagedMixin, ExpiringModel):
class DeviceAuthenticationToken(ExpiringModel):
identifier = models.UUIDField(default=uuid4, primary_key=True)
device = models.ForeignKey(Device, on_delete=models.CASCADE)
@@ -160,7 +160,7 @@ class DeviceAuthenticationToken(InternallyManagedMixin, ExpiringModel):
verbose_name_plural = _("Device authentication tokens")
class AppleNonce(InternallyManagedMixin, ExpiringModel):
class AppleNonce(ExpiringModel):
nonce = models.TextField()
device_token = models.ForeignKey(DeviceToken, on_delete=models.CASCADE)

View File

@@ -15,7 +15,7 @@ from authentik.core.models import AttributesMixin, ExpiringModel
from authentik.flows.models import Stage
from authentik.flows.stage import StageView
from authentik.lib.merge import MERGE_LIST_UNIQUE
from authentik.lib.models import InheritanceForeignKey, InternallyManagedMixin, SerializerModel
from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
from authentik.policies.models import PolicyBinding, PolicyBindingModel
from authentik.tasks.schedules.common import ScheduleSpec
@@ -28,7 +28,7 @@ LOGGER = get_logger()
DEVICE_FACTS_CACHE_TIMEOUT = 3600
class Device(InternallyManagedMixin, ExpiringModel, AttributesMixin, PolicyBindingModel):
class Device(ExpiringModel, AttributesMixin, PolicyBindingModel):
device_uuid = models.UUIDField(default=uuid4, primary_key=True)
name = models.TextField(unique=True)
@@ -86,7 +86,7 @@ class DeviceUserBinding(PolicyBinding):
verbose_name_plural = _("Device User bindings")
class DeviceConnection(InternallyManagedMixin, SerializerModel):
class DeviceConnection(SerializerModel):
device_connection_uuid = models.UUIDField(default=uuid4, primary_key=True)
device = models.ForeignKey("Device", on_delete=models.CASCADE)
connector = models.ForeignKey("Connector", on_delete=models.CASCADE)
@@ -115,7 +115,7 @@ class DeviceConnection(InternallyManagedMixin, SerializerModel):
verbose_name_plural = _("Device connections")
class DeviceFactSnapshot(InternallyManagedMixin, ExpiringModel, SerializerModel):
class DeviceFactSnapshot(ExpiringModel, SerializerModel):
snapshot_id = models.UUIDField(primary_key=True, default=uuid4)
connection = models.ForeignKey(DeviceConnection, on_delete=models.CASCADE)
data = models.JSONField(default=dict)

View File

@@ -11,7 +11,7 @@ from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer
from authentik.core.models import ExpiringModel
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
if TYPE_CHECKING:
from authentik.enterprise.license import LicenseKey
@@ -81,7 +81,7 @@ class LicenseUsageStatus(models.TextChoices):
return self in [LicenseUsageStatus.VALID, LicenseUsageStatus.EXPIRY_SOON]
class LicenseUsage(InternallyManagedMixin, ExpiringModel):
class LicenseUsage(ExpiringModel):
"""a single license usage record"""
expires = models.DateTimeField(default=usage_expiry)

View File

@@ -18,7 +18,7 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider
@@ -32,7 +32,7 @@ def default_scopes() -> list[str]:
]
class GoogleWorkspaceProviderUser(InternallyManagedMixin, SerializerModel):
class GoogleWorkspaceProviderUser(SerializerModel):
"""Mapping of a user and provider to a Google user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@@ -58,7 +58,7 @@ class GoogleWorkspaceProviderUser(InternallyManagedMixin, SerializerModel):
return f"Google Workspace Provider User {self.user_id} to {self.provider_id}"
class GoogleWorkspaceProviderGroup(InternallyManagedMixin, SerializerModel):
class GoogleWorkspaceProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Google group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)

View File

@@ -18,12 +18,12 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider
class MicrosoftEntraProviderUser(InternallyManagedMixin, SerializerModel):
class MicrosoftEntraProviderUser(SerializerModel):
"""Mapping of a user and provider to a Microsoft user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@@ -49,7 +49,7 @@ class MicrosoftEntraProviderUser(InternallyManagedMixin, SerializerModel):
return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}"
class MicrosoftEntraProviderGroup(InternallyManagedMixin, SerializerModel):
class MicrosoftEntraProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Microsoft group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)

View File

@@ -14,7 +14,7 @@ from jwt import encode
from authentik.core.models import BackchannelProvider, ExpiringModel, Token
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import CreatedUpdatedModel, InternallyManagedMixin
from authentik.lib.models import CreatedUpdatedModel
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
from authentik.tasks.models import TasksModel
@@ -153,7 +153,7 @@ class Stream(models.Model):
return encode(data, key, algorithm=alg, headers=headers)
class StreamEvent(InternallyManagedMixin, CreatedUpdatedModel, ExpiringModel):
class StreamEvent(CreatedUpdatedModel, ExpiringModel):
"""Single stream event to be sent"""
uuid = models.UUIDField(default=uuid4, primary_key=True, editable=False)

View File

@@ -11,7 +11,7 @@ from rest_framework.serializers import BaseSerializer, Serializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.flows.stage import StageView
from authentik.lib.models import DeprecatedMixin, InternallyManagedMixin, SerializerModel
from authentik.lib.models import DeprecatedMixin, SerializerModel
from authentik.stages.authenticator.models import Device
@@ -63,7 +63,7 @@ class AuthenticatorEndpointGDTCStage(DeprecatedMixin, ConfigurableStage, Friendl
verbose_name_plural = _("Endpoint Authenticator Google Device Trust Connector Stages")
class EndpointDevice(InternallyManagedMixin, SerializerModel, Device):
class EndpointDevice(SerializerModel, Device):
"""Endpoint Device for a single user"""
uuid = models.UUIDField(primary_key=True, default=uuid4)
@@ -91,7 +91,7 @@ class EndpointDevice(InternallyManagedMixin, SerializerModel, Device):
verbose_name_plural = _("Endpoint Devices")
class EndpointDeviceConnection(InternallyManagedMixin, models.Model):
class EndpointDeviceConnection(models.Model):
device = models.ForeignKey(EndpointDevice, on_delete=models.CASCADE)
stage = models.ForeignKey(AuthenticatorEndpointGDTCStage, on_delete=models.CASCADE)

View File

@@ -19,7 +19,6 @@ from authentik.blueprints.v1.importer import excluded_models
from authentik.core.models import Group, User
from authentik.events.models import Event, EventAction, Notification
from authentik.events.utils import model_to_dict
from authentik.lib.models import InternallyManagedMixin
from authentik.lib.sentry import should_ignore_exception
from authentik.lib.utils.errors import exception_to_dict
from authentik.stages.authenticator_static.models import StaticToken
@@ -41,7 +40,7 @@ _CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_events_log_request", de
def should_log_model(model: Model) -> bool:
"""Return true if operation on `model` should be logged"""
return model.__class__ not in IGNORED_MODELS and not isinstance(model, InternallyManagedMixin)
return model.__class__ not in IGNORED_MODELS
def should_log_m2m(model: Model) -> bool:

View File

@@ -1,18 +0,0 @@
from typing import cast
from django.contrib.auth.models import AnonymousUser
from rest_framework.authentication import BaseAuthentication
from rest_framework.request import Request
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
class FlowActive(BaseAuthentication):
"""Authenticate requests when a flow is currently active"""
def authenticate(self, request: Request):
plan = cast(FlowPlan | None, request.session.get(SESSION_KEY_PLAN))
if not plan:
return None
return (plan.context.get(PLAN_CONTEXT_PENDING_USER, AnonymousUser()), plan)

View File

@@ -20,7 +20,7 @@ from authentik.core.models import Token
from authentik.core.types import UserSettingSerializer
from authentik.flows.challenge import FlowLayout
from authentik.lib.config import CONFIG
from authentik.lib.models import InheritanceForeignKey, InternallyManagedMixin, SerializerModel
from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.lib.utils.reflection import class_to_path
from authentik.policies.models import PolicyBindingModel
@@ -301,7 +301,7 @@ class FriendlyNamedStage(models.Model):
abstract = True
class FlowToken(InternallyManagedMixin, Token):
class FlowToken(Token):
"""Subclass of a standard Token, stores the currently active flow plan upon creation.
Can be used to later resume a flow."""

View File

@@ -48,9 +48,6 @@ class FlowTestCase(APITestCase):
self.assertEqual(raw_response[key], expected)
return raw_response
def get_flow_plan(self) -> FlowPlan | None:
return self.client.session.get(SESSION_KEY_PLAN)
def assertStageRedirects(self, response: HttpResponse, to: str) -> dict[str, Any]:
"""Wrapper around assertStageResponse that checks for a redirect"""
return self.assertStageResponse(response, component="xak-flow-redirect", to=to)

View File

@@ -147,8 +147,6 @@ class FlowExecutorView(APIView):
token.delete()
if not isinstance(plan, FlowPlan):
return None
if existing_plan := self.request.session.get(SESSION_KEY_PLAN):
plan.context.update(existing_plan.context)
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
self._logger.debug("f(exec): restored flow plan from token", plan=plan)
return plan

View File

@@ -111,7 +111,6 @@ def get_logger_config():
"hpack": "WARNING",
"httpx": "WARNING",
"azure": "WARNING",
"httpcore": "WARNING",
}
for handler_name, level in handler_level_map.items():
base_config["loggers"][handler_name] = {

View File

@@ -64,10 +64,6 @@ class DeprecatedMixin:
"""Mixin for classes that are deprecated"""
class InternallyManagedMixin:
"""Mixin for models that should _not_ be manageable via blueprint."""
class DomainlessURLValidator(URLValidator):
"""Subclass of URLValidator which doesn't check the domain
(to allow hostnames without domain)"""

View File

@@ -1,61 +1,25 @@
from json import JSONDecodeError
from authentik.lib.sentry import SentryIgnoredException
class BaseSyncException(SentryIgnoredException):
"""Base class for all sync exceptions"""
error_prefix = "Sync error"
error_default = "Error communicating with remote system"
def __init__(self, response=None):
super().__init__()
self.response = response
def __str__(self):
if self.response is not None:
if hasattr(self.response, "json"):
try:
return f"{self.error_prefix}: {self.response.json()}"
except JSONDecodeError:
pass
if hasattr(self.response, "text"):
return f"{self.error_prefix}: {self.response.text}"
return f"{self.error_prefix}: {self.response}"
return self.error_default
def __repr__(self):
return self.__str__()
class TransientSyncException(BaseSyncException):
"""Transient sync exception which may be caused by network blips, etc"""
error_prefix = "Network error"
error_default = "Network error communicating with remote system"
class NotFoundSyncException(BaseSyncException):
"""Exception when an object was not found in the remote system"""
error_prefix = "Object not found"
error_default = "Object not found in remote system"
class ObjectExistsSyncException(BaseSyncException):
"""Exception when an object already exists in the remote system"""
error_prefix = "Object exists"
error_default = "Object exists in remote system"
class BadRequestSyncException(BaseSyncException):
"""Exception when invalid data was sent to the remote system"""
error_prefix = "Bad request"
error_default = "Bad request to remote system"
class DryRunRejected(BaseSyncException):
"""When dry_run is enabled and a provider dropped a mutating request"""

View File

@@ -9,7 +9,6 @@ from rest_framework.serializers import BaseSerializer
from structlog.stdlib import get_logger
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.blueprints.v1.meta.registry import BaseMetaModel
from authentik.events.models import Event, EventAction
from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult
@@ -32,7 +31,7 @@ def model_choices() -> list[tuple[str, str]]:
Returns a list of tuples containing (dotted.model.path, name)"""
choices = []
for model in apps.get_models():
if not is_model_allowed(model) or issubclass(model, BaseMetaModel):
if not is_model_allowed(model):
continue
name = f"{model._meta.app_label}.{model._meta.model_name}"
choices.append((name, model._meta.verbose_name))

View File

@@ -14,7 +14,7 @@ from structlog import get_logger
from authentik.core.models import ExpiringModel
from authentik.lib.config import CONFIG
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.root.middleware import ClientIPMiddleware
@@ -69,7 +69,7 @@ class ReputationPolicy(Policy):
verbose_name_plural = _("Reputation Policies")
class Reputation(InternallyManagedMixin, ExpiringModel, SerializerModel):
class Reputation(ExpiringModel, SerializerModel):
"""Reputation for user and or IP."""
objects = PostgresManager()

View File

@@ -42,7 +42,7 @@ from authentik.core.models import (
)
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
from authentik.lib.models import DomainlessURLValidator, InternallyManagedMixin, SerializerModel
from authentik.lib.models import DomainlessURLValidator, SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
from authentik.providers.oauth2.constants import SubModes
from authentik.sources.oauth.models import OAuthSource
@@ -462,7 +462,7 @@ class BaseGrantModel(models.Model):
self._scope = " ".join(value)
class AuthorizationCode(InternallyManagedMixin, SerializerModel, ExpiringModel, BaseGrantModel):
class AuthorizationCode(SerializerModel, ExpiringModel, BaseGrantModel):
"""OAuth2 Authorization Code"""
code = models.CharField(max_length=255, unique=True, verbose_name=_("Code"))
@@ -497,7 +497,7 @@ class AuthorizationCode(InternallyManagedMixin, SerializerModel, ExpiringModel,
)
class AccessToken(InternallyManagedMixin, SerializerModel, ExpiringModel, BaseGrantModel):
class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
"""OAuth2 access token, non-opaque using a JWT as identifier"""
token = models.TextField()
@@ -545,7 +545,7 @@ class AccessToken(InternallyManagedMixin, SerializerModel, ExpiringModel, BaseGr
return TokenModelSerializer
class RefreshToken(InternallyManagedMixin, SerializerModel, ExpiringModel, BaseGrantModel):
class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
"""OAuth2 Refresh Token, opaque"""
token = models.TextField(default=generate_client_secret)
@@ -585,7 +585,7 @@ class RefreshToken(InternallyManagedMixin, SerializerModel, ExpiringModel, BaseG
return TokenModelSerializer
class DeviceToken(InternallyManagedMixin, ExpiringModel):
class DeviceToken(ExpiringModel):
"""Temporary device token for OAuth device flow"""
user = models.ForeignKey(

View File

@@ -13,7 +13,7 @@ from rest_framework.serializers import Serializer
from authentik.core.models import ExpiringModel
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator, InternallyManagedMixin
from authentik.lib.models import DomainlessURLValidator
from authentik.outposts.models import OutpostModel
from authentik.providers.oauth2.models import (
ClientTypes,
@@ -27,7 +27,7 @@ SCOPE_AK_PROXY = "ak_proxy"
OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"
class ProxySession(InternallyManagedMixin, ExpiringModel):
class ProxySession(ExpiringModel):
"""Session storage for proxyv2 outposts using PostgreSQL"""
uuid = models.UUIDField(default=uuid4, primary_key=True)

View File

@@ -15,7 +15,7 @@ from structlog.stdlib import get_logger
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User, default_token_key
from authentik.events.models import Event, EventAction
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
from authentik.outposts.models import OutpostModel
from authentik.policies.models import PolicyBindingModel
@@ -145,7 +145,7 @@ class RACPropertyMapping(PropertyMapping):
verbose_name_plural = _("RAC Provider Property Mappings")
class ConnectionToken(InternallyManagedMixin, ExpiringModel):
class ConnectionToken(ExpiringModel):
"""Token for a single connection to a specified endpoint"""
connection_token_uuid = models.UUIDField(default=uuid4, primary_key=True)

View File

@@ -18,7 +18,7 @@ from authentik.core.models import (
User,
)
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator, InternallyManagedMixin, SerializerModel
from authentik.lib.models import DomainlessURLValidator, SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.models import SAMLNameIDPolicy
from authentik.sources.saml.processors.constants import (
@@ -303,7 +303,7 @@ class SAMLProviderImportModel(CreatableType, Provider):
verbose_name_plural = _("SAML Providers from Metadata")
class SAMLSession(InternallyManagedMixin, SerializerModel, ExpiringModel):
class SAMLSession(SerializerModel, ExpiringModel):
"""Track active SAML sessions for Single Logout support"""
saml_session_id = models.UUIDField(default=uuid4, primary_key=True)

View File

@@ -14,7 +14,6 @@ class SCIMRequestException(TransientSyncException):
_message: str | None
def __init__(self, response: Response | None = None, message: str | None = None) -> None:
super().__init__(response)
self._response = response
self._message = message

View File

@@ -13,7 +13,7 @@ from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
@@ -22,7 +22,7 @@ from authentik.providers.scim.clients.auth import SCIMTokenAuth
LOGGER = get_logger()
class SCIMProviderUser(InternallyManagedMixin, SerializerModel):
class SCIMProviderUser(SerializerModel):
"""Mapping of a user and provider to a SCIM user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@@ -44,7 +44,7 @@ class SCIMProviderUser(InternallyManagedMixin, SerializerModel):
return f"SCIM Provider User {self.user_id} to {self.provider_id}"
class SCIMProviderGroup(InternallyManagedMixin, SerializerModel):
class SCIMProviderGroup(SerializerModel):
"""Mapping of a group and provider to a SCIM user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)

View File

@@ -2,7 +2,7 @@
from django.contrib.auth.models import Permission
from django.http import Http404
from django_filters.filters import AllValuesMultipleFilter, BooleanFilter
from django_filters.filters import AllValuesMultipleFilter, BooleanFilter, CharFilter, NumberFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
@@ -22,7 +22,7 @@ from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User
from authentik.core.models import Group, User
from authentik.rbac.decorators import permission_required
from authentik.rbac.models import Role, get_permission_choices
@@ -65,15 +65,63 @@ class RoleSerializer(ManagedSerializer, ModelSerializer):
class RoleFilterSet(FilterSet):
"""Filter for PropertyMapping"""
"""Filter for Role"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
managed__isnull = BooleanFilter(field_name="managed", lookup_expr="isnull")
inherited = BooleanFilter(
method="filter_inherited",
label="Include inherited roles (requires users or ak_groups filter)",
)
users = extend_schema_field(OpenApiTypes.INT)(
NumberFilter(
method="filter_users",
label="Filter by user (use with inherited=true for all roles)",
)
)
ak_groups = extend_schema_field(OpenApiTypes.UUID)(
CharFilter(
method="filter_ak_groups",
label="Filter by group (use with inherited=true for all roles)",
)
)
def filter_inherited(self, queryset, name, value):
"""This filter is handled by filter_users and filter_ak_groups"""
return queryset
def filter_users(self, queryset, name, value):
"""Filter roles by user, optionally including inherited roles"""
user = User.objects.filter(pk=value).first()
if not user:
return queryset.none()
include_inherited = self.data.get("inherited", "").lower() == "true"
if include_inherited:
return user.all_roles()
return queryset.filter(users=user)
def filter_ak_groups(self, queryset, name, value):
"""Filter roles by group, optionally including inherited roles"""
group = Group.objects.filter(pk=value).first()
if not group:
return queryset.none()
include_inherited = self.data.get("inherited", "").lower() == "true"
if include_inherited:
return group.all_roles()
return queryset.filter(ak_groups=group)
class Meta:
model = Role
fields = ["name", "users", "managed"]
fields = [
"name",
"managed",
]
class RoleViewSet(UsedByMixin, ModelViewSet):

View File

@@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import BaseSerializer, Serializer
from authentik.core.models import Group, PropertyMapping, Source, Token, User
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
class SCIMSource(Source):
@@ -101,7 +101,7 @@ class SCIMSourcePropertyMapping(PropertyMapping):
verbose_name_plural = _("SCIM Source Property Mappings")
class SCIMSourceUser(InternallyManagedMixin, SerializerModel):
class SCIMSourceUser(SerializerModel):
"""Mapping of a user and source to a SCIM user ID"""
id = models.TextField(primary_key=True, default=uuid4)
@@ -127,7 +127,7 @@ class SCIMSourceUser(InternallyManagedMixin, SerializerModel):
return f"SCIM User {self.user_id} to {self.source_id}"
class SCIMSourceGroup(InternallyManagedMixin, SerializerModel):
class SCIMSourceGroup(SerializerModel):
"""Mapping of a group and source to a SCIM user ID"""
id = models.TextField(primary_key=True, default=uuid4)

View File

@@ -9,7 +9,7 @@ from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField, IntegerField
from rest_framework.permissions import AllowAny
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet
@@ -21,11 +21,9 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import User
from authentik.flows.api.stages import StageSerializer
from authentik.flows.auth import FlowActive
from authentik.flows.planner import FlowPlan
from authentik.rbac.decorators import permission_required
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import PLAN_CONTEXT_DUO_ENROLL
from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL
LOGGER = get_logger()
@@ -86,20 +84,14 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
),
},
)
@action(
methods=["POST"],
detail=True,
authentication_classes=[FlowActive],
permission_classes=[AllowAny],
)
@action(methods=["POST"], detail=True, permission_classes=[IsAuthenticated])
def enrollment_status(self, request: Request, pk: str) -> Response:
"""Check enrollment status of user details in current session"""
stage: AuthenticatorDuoStage = AuthenticatorDuoStage.objects.filter(pk=pk).first()
if not stage:
raise Http404
client = stage.auth_client()
plan: FlowPlan = request.auth
enroll = plan.context.get(PLAN_CONTEXT_DUO_ENROLL)
enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL)
if not enroll:
return Response(status=400)
status = client.enroll_status(enroll["user_id"], enroll["activation_code"])

View File

@@ -14,7 +14,7 @@ from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import InvalidStageError
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
PLAN_CONTEXT_DUO_ENROLL = "goauthentik.io/stages/authenticator_duo/enroll"
SESSION_KEY_DUO_ENROLL = "authentik/stages/authenticator_duo/enroll"
class AuthenticatorDuoChallenge(WithUserInfoChallenge):
@@ -50,14 +50,14 @@ class AuthenticatorDuoStageView(ChallengeStageView):
user=user,
).from_http(self.request, user)
raise InvalidStageError(str(exc)) from exc
self.executor.plan.context[PLAN_CONTEXT_DUO_ENROLL] = enroll
self.request.session[SESSION_KEY_DUO_ENROLL] = enroll
return enroll
def get_challenge(self, *args, **kwargs) -> Challenge:
stage: AuthenticatorDuoStage = self.executor.current_stage
if PLAN_CONTEXT_DUO_ENROLL not in self.executor.plan.context:
if SESSION_KEY_DUO_ENROLL not in self.request.session:
self.duo_enroll()
enroll = self.executor.plan.context[PLAN_CONTEXT_DUO_ENROLL]
enroll = self.request.session[SESSION_KEY_DUO_ENROLL]
return AuthenticatorDuoChallenge(
data={
"activation_barcode": enroll["activation_barcode"],
@@ -69,14 +69,14 @@ class AuthenticatorDuoStageView(ChallengeStageView):
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
# Duo Challenge has already been validated
stage: AuthenticatorDuoStage = self.executor.current_stage
enroll = self.executor.plan.context.get(PLAN_CONTEXT_DUO_ENROLL)
enroll = self.request.session.get(SESSION_KEY_DUO_ENROLL)
enroll_status = stage.auth_client().enroll_status(
enroll["user_id"], enroll["activation_code"]
)
if enroll_status != "success":
return self.executor.stage_invalid(f"Invalid enrollment status: {enroll_status}.")
existing_device = DuoDevice.objects.filter(duo_user_id=enroll["user_id"]).first()
self.executor.plan.context.pop(PLAN_CONTEXT_DUO_ENROLL)
self.request.session.pop(SESSION_KEY_DUO_ENROLL)
if not existing_device:
DuoDevice.objects.create(
name="Duo Authenticator",
@@ -88,3 +88,6 @@ class AuthenticatorDuoStageView(ChallengeStageView):
else:
return self.executor.stage_invalid("Device with Credential ID already exists.")
return self.executor.stage_ok()
def cleanup(self):
self.request.session.pop(SESSION_KEY_DUO_ENROLL, None)

View File

@@ -11,6 +11,7 @@ from authentik.flows.models import FlowStageBinding
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL
from authentik.stages.identification.models import IdentificationStage, UserFields
@@ -50,6 +51,42 @@ class AuthenticatorDuoStageTests(FlowTestCase):
)
self.assertEqual(response.status_code, 404)
def test_api_enrollment(self):
"""Test `enrollment_status`"""
self.client.force_login(self.user)
stage = AuthenticatorDuoStage.objects.create(
name=generate_id(),
client_id=generate_id(),
client_secret=generate_id(),
api_hostname=generate_id(),
)
response = self.client.post(
reverse(
"authentik_api:authenticatorduostage-enrollment-status",
kwargs={
"pk": str(stage.pk),
},
)
)
self.assertEqual(response.status_code, 400)
session = self.client.session
session[SESSION_KEY_DUO_ENROLL] = {"user_id": "foo", "activation_code": "bar"}
session.save()
with patch("duo_client.auth.Auth.enroll_status", MagicMock(return_value="foo")):
response = self.client.post(
reverse(
"authentik_api:authenticatorduostage-enrollment-status",
kwargs={
"pk": str(stage.pk),
},
)
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content.decode(), '{"duo_response":"foo"}')
def test_api_import_manual_invalid_username(self):
"""Test `import_device_manual`"""
self.client.force_login(self.user)
@@ -277,17 +314,6 @@ class AuthenticatorDuoStageTests(FlowTestCase):
self.assertEqual(enroll_mock.call_count, 1)
with patch("duo_client.auth.Auth.enroll_status", MagicMock(return_value="success")):
response = self.client.post(
reverse(
"authentik_api:authenticatorduostage-enrollment-status",
kwargs={
"pk": str(stage.pk),
},
)
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(response.content, {"duo_response": "success"})
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), {}
)

View File

@@ -26,7 +26,7 @@ from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
PLAN_CONTEXT_EMAIL_DEVICE = "goauthentik.io/stages/authenticator_email/email_device"
SESSION_KEY_EMAIL_DEVICE = "authentik/stages/authenticator_email/email_device"
PLAN_CONTEXT_EMAIL = "email"
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
PLAN_CONTEXT_EMAIL_OVERRIDE = "email"
@@ -79,7 +79,7 @@ class AuthenticatorEmailStageView(ChallengeStageView):
if EmailDevice.objects.filter(Q(email=email), stage=stage.pk).exists():
raise ValidationError(_("Invalid email"))
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
try:
message = TemplateEmailMessage(
@@ -116,9 +116,9 @@ class AuthenticatorEmailStageView(ChallengeStageView):
self.logger.debug("got email from plan context")
return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_EMAIL)
# Check device for email
if PLAN_CONTEXT_EMAIL_DEVICE in self.executor.plan.context:
if SESSION_KEY_EMAIL_DEVICE in self.request.session:
self.logger.debug("got email from device in session")
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
if device.email == "":
return None
return device.email
@@ -135,7 +135,7 @@ class AuthenticatorEmailStageView(ChallengeStageView):
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
response = super().get_response_instance(data)
response.device = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
response.device = self.request.session[SESSION_KEY_EMAIL_DEVICE]
return response
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@@ -147,11 +147,11 @@ class AuthenticatorEmailStageView(ChallengeStageView):
return self.executor.stage_invalid(
_("The user already has an email address registered for MFA.")
)
if PLAN_CONTEXT_EMAIL_DEVICE not in self.executor.plan.context:
if SESSION_KEY_EMAIL_DEVICE not in self.request.session:
device = EmailDevice(user=user, confirmed=False, stage=stage, name="Email Device")
valid_secs: int = timedelta_from_string(stage.token_expiry).total_seconds()
device.generate_token(valid_secs=valid_secs, commit=False)
self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE] = device
self.request.session[SESSION_KEY_EMAIL_DEVICE] = device
if email := self._has_email():
device.email = email
try:
@@ -165,16 +165,16 @@ class AuthenticatorEmailStageView(ChallengeStageView):
self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(
PLAN_CONTEXT_EMAIL, None
)
self.executor.plan.context.pop(PLAN_CONTEXT_EMAIL_DEVICE, None)
self.request.session.pop(SESSION_KEY_EMAIL_DEVICE, None)
self.logger.warning("failed to send email to pre-set address", exc=exc)
return self.get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
"""Email Token is validated by challenge"""
device: EmailDevice = self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
device: EmailDevice = self.request.session[SESSION_KEY_EMAIL_DEVICE]
if not device.confirmed:
return self.challenge_invalid(response)
device.save()
del self.executor.plan.context[PLAN_CONTEXT_EMAIL_DEVICE]
del self.request.session[SESSION_KEY_EMAIL_DEVICE]
return self.executor.stage_ok()

View File

@@ -11,7 +11,7 @@ from django.template.exceptions import TemplateDoesNotExist
from django.urls import reverse
from django.utils.timezone import now
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
from authentik.flows.models import FlowStageBinding
from authentik.flows.tests import FlowTestCase
from authentik.lib.config import CONFIG
@@ -21,7 +21,9 @@ from authentik.stages.authenticator_email.api import (
EmailDeviceSerializer,
)
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice
from authentik.stages.authenticator_email.stage import PLAN_CONTEXT_EMAIL_DEVICE
from authentik.stages.authenticator_email.stage import (
SESSION_KEY_EMAIL_DEVICE,
)
from authentik.stages.email.utils import TemplateEmailMessage
@@ -31,7 +33,7 @@ class TestAuthenticatorEmailStage(FlowTestCase):
def setUp(self):
super().setUp()
self.flow = create_test_flow()
self.user = create_test_user()
self.user = create_test_admin_user()
self.user_noemail = create_test_user(email="")
self.stage = AuthenticatorEmailStage.objects.create(
name="email-authenticator",
@@ -211,26 +213,20 @@ class TestAuthenticatorEmailStage(FlowTestCase):
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
data={"component": "ak-stage-authenticator-email"},
)
self.assertStageResponse(
response,
self.flow,
response_errors={"non_field_errors": [{"code": "invalid", "string": "email required"}]},
)
self.assertIn("email required", str(response.content))
# Test invalid code
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
data={"component": "ak-stage-authenticator-email", "code": "000000"},
)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"code": "invalid", "string": "Code does not match"}]
},
)
self.assertIn("Code does not match", str(response.content))
# Test valid code
self.client.force_login(self.user)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
device = self.device
token = device.token
response = self.client.post(
@@ -289,7 +285,8 @@ class TestAuthenticatorEmailStage(FlowTestCase):
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
device = self.get_flow_plan().context[PLAN_CONTEXT_EMAIL_DEVICE]
self.assertIn(SESSION_KEY_EMAIL_DEVICE, self.client.session)
device = self.client.session[SESSION_KEY_EMAIL_DEVICE]
self.assertIsInstance(device, EmailDevice)
self.assertFalse(device.confirmed)
self.assertEqual(device.user, self.user)
@@ -297,6 +294,8 @@ class TestAuthenticatorEmailStage(FlowTestCase):
# Test device confirmation and cleanup
device.confirmed = True
device.email = "new_test@authentik.local" # Use a different email
self.client.session[SESSION_KEY_EMAIL_DEVICE] = device
self.client.session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
data={"component": "ak-stage-authenticator-email", "code": device.token},

View File

@@ -20,7 +20,7 @@ from authentik.stages.authenticator_sms.models import (
)
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
PLAN_CONTEXT_SMS_DEVICE = "goauthentik.io/stages/authenticator_sms/sms_device"
SESSION_KEY_SMS_DEVICE = "authentik/stages/authenticator_sms/sms_device"
PLAN_CONTEXT_PHONE = "phone"
@@ -70,7 +70,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
raise ValidationError(_("Invalid phone number"))
# No code yet, but we have a phone number, so send a verification message
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
stage.send(self.request, device.token, device)
def _has_phone_number(self) -> str | None:
@@ -78,9 +78,9 @@ class AuthenticatorSMSStageView(ChallengeStageView):
if PLAN_CONTEXT_PHONE in context.get(PLAN_CONTEXT_PROMPT, {}):
self.logger.debug("got phone number from plan context")
return context.get(PLAN_CONTEXT_PROMPT, {}).get(PLAN_CONTEXT_PHONE)
if PLAN_CONTEXT_SMS_DEVICE in self.executor.plan.context:
if SESSION_KEY_SMS_DEVICE in self.request.session:
self.logger.debug("got phone number from device in session")
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
if device.phone_number == "":
return None
return device.phone_number
@@ -95,7 +95,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
response = super().get_response_instance(data)
response.device = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
response.device = self.request.session[SESSION_KEY_SMS_DEVICE]
return response
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@@ -103,10 +103,10 @@ class AuthenticatorSMSStageView(ChallengeStageView):
stage: AuthenticatorSMSStage = self.executor.current_stage
if PLAN_CONTEXT_SMS_DEVICE not in self.executor.plan.context:
if SESSION_KEY_SMS_DEVICE not in self.request.session:
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
device.generate_token(commit=False)
self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE] = device
self.request.session[SESSION_KEY_SMS_DEVICE] = device
if phone_number := self._has_phone_number():
device.phone_number = phone_number
try:
@@ -120,14 +120,14 @@ class AuthenticatorSMSStageView(ChallengeStageView):
self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).pop(
PLAN_CONTEXT_PHONE, None
)
self.executor.plan.context.pop(PLAN_CONTEXT_SMS_DEVICE, None)
self.request.session.pop(SESSION_KEY_SMS_DEVICE, None)
self.logger.warning("failed to send SMS message to pre-set number", exc=exc)
return self.get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
"""SMS Token is validated by challenge"""
device: SMSDevice = self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE]
if not device.confirmed:
return self.challenge_invalid(response)
stage: AuthenticatorSMSStage = self.executor.current_stage
@@ -135,5 +135,5 @@ class AuthenticatorSMSStageView(ChallengeStageView):
self.logger.debug("Hashing number on device")
device.set_hashed_number()
device.save()
del self.executor.plan.context[PLAN_CONTEXT_SMS_DEVICE]
del self.request.session[SESSION_KEY_SMS_DEVICE]
return self.executor.stage_ok()

View File

@@ -18,7 +18,7 @@ from authentik.stages.authenticator_sms.models import (
SMSProviders,
hash_phone_number,
)
from authentik.stages.authenticator_sms.stage import PLAN_CONTEXT_PHONE, PLAN_CONTEXT_SMS_DEVICE
from authentik.stages.authenticator_sms.stage import PLAN_CONTEXT_PHONE, SESSION_KEY_SMS_DEVICE
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
@@ -125,7 +125,7 @@ class AuthenticatorSMSStageTests(FlowTestCase):
self.assertEqual(mocker.call_count, 1)
self.assertEqual(mocker.request_history[0].method, "POST")
request_body = dict(parse_qsl(mocker.request_history[0].body))
device: SMSDevice = self.get_flow_plan().context[PLAN_CONTEXT_SMS_DEVICE]
device: SMSDevice = self.client.session[SESSION_KEY_SMS_DEVICE]
self.assertEqual(
request_body,
{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@ from webauthn.helpers.structs import PublicKeyCredentialDescriptor
from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.stages.authenticator.models import Device
UNKNOWN_DEVICE_TYPE_AAGUID = "00000000-0000-0000-0000-000000000000"
@@ -164,7 +164,7 @@ class WebAuthnDevice(SerializerModel, Device):
verbose_name_plural = _("WebAuthn Devices")
class WebAuthnDeviceType(InternallyManagedMixin, SerializerModel):
class WebAuthnDeviceType(SerializerModel):
"""WebAuthn device type, used to restrict which device types are allowed"""
aaguid = models.UUIDField(primary_key=True, unique=True)

View File

@@ -7,7 +7,7 @@ from rest_framework.serializers import BaseSerializer, Serializer
from authentik.core.models import Application, ExpiringModel, User
from authentik.flows.models import Stage
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
@@ -51,7 +51,7 @@ class ConsentStage(Stage):
verbose_name_plural = _("Consent Stages")
class UserConsent(InternallyManagedMixin, SerializerModel, ExpiringModel):
class UserConsent(SerializerModel, ExpiringModel):
"""Consent given by a user for an application"""
user = models.ForeignKey(User, on_delete=models.CASCADE)

View File

@@ -1,6 +1,5 @@
"""authentik consent stage"""
from hmac import compare_digest
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
@@ -24,7 +23,7 @@ PLAN_CONTEXT_CONSENT = "consent"
PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS = "consent_additional_permissions"
PLAN_CONTEXT_CONSENT_TOKEN = "goauthentik.io/stages/consent/token" # nosec
SESSION_KEY_CONSENT_TOKEN = "authentik/stages/consent/token" # nosec
class ConsentPermissionSerializer(PassiveSerializer):
@@ -51,9 +50,7 @@ class ConsentChallengeResponse(ChallengeResponse):
token = CharField(required=True)
def validate_token(self, token: str):
if not compare_digest(
token, self.stage.executor.plan.context.get(PLAN_CONTEXT_CONSENT_TOKEN, "")
):
if token != self.stage.executor.request.session[SESSION_KEY_CONSENT_TOKEN]:
raise ValidationError(_("Invalid consent token, re-showing prompt"))
return token
@@ -65,7 +62,7 @@ class ConsentStageView(ChallengeStageView):
def get_challenge(self) -> Challenge:
token = str(uuid4())
self.executor.plan.context[PLAN_CONTEXT_CONSENT_TOKEN] = token
self.request.session[SESSION_KEY_CONSENT_TOKEN] = token
data = {
"permissions": self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_PERMISSIONS, []),
"additional_permissions": self.executor.plan.context.get(

View File

@@ -19,7 +19,7 @@ from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConse
from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS,
PLAN_CONTEXT_CONSENT_TOKEN,
SESSION_KEY_CONSENT_TOKEN,
)
@@ -83,10 +83,11 @@ class TestConsentStage(FlowTestCase):
)
self.assertEqual(response.status_code, 200)
session = self.client.session
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
"token": session[SESSION_KEY_CONSENT_TOKEN],
},
)
@@ -121,7 +122,7 @@ class TestConsentStage(FlowTestCase):
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
{
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
"token": session[SESSION_KEY_CONSENT_TOKEN],
},
)
self.assertEqual(response.status_code, 200)

View File

@@ -19,7 +19,7 @@ from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TOKEN
from authentik.stages.consent.stage import SESSION_KEY_CONSENT_TOKEN
from authentik.stages.email.models import EmailStage
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView
@@ -174,7 +174,7 @@ class TestEmailStage(FlowTestCase):
kwargs={"flow_slug": self.flow.slug},
),
data={
"token": self.get_flow_plan().context[PLAN_CONTEXT_CONSENT_TOKEN],
"token": self.client.session[SESSION_KEY_CONSENT_TOKEN],
},
follow=True,
)

View File

@@ -10,7 +10,7 @@ from django_dramatiq_postgres.models import TaskBase, TaskState
from authentik.events.logs import LogEvent
from authentik.events.utils import sanitize_item
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.utils.errors import exception_to_dict
from authentik.tenants.models import Tenant
@@ -30,7 +30,7 @@ class TaskStatus(models.TextChoices):
ERROR = "error"
class Task(InternallyManagedMixin, SerializerModel, TaskBase):
class Task(SerializerModel, TaskBase):
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
@@ -144,7 +144,7 @@ class Task(InternallyManagedMixin, SerializerModel, TaskBase):
self.log(self.uid, TaskStatus.ERROR, message, **attributes)
class TaskLog(InternallyManagedMixin, models.Model):
class TaskLog(models.Model):
id = models.UUIDField(default=uuid4, primary_key=True, editable=False)
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="tasklogs")

View File

@@ -17,7 +17,7 @@ from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.blueprints.apps import ManagedAppConfig
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
LOGGER = get_logger()
@@ -41,7 +41,7 @@ def _validate_schema_name(name):
)
class Tenant(InternallyManagedMixin, TenantMixin, SerializerModel):
class Tenant(TenantMixin, SerializerModel):
"""Tenant"""
tenant_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)

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 2026.2.0-rc1 Blueprint schema",
"title": "authentik 2025.12.0-rc3 Blueprint schema",
"required": [
"version",
"entries"

View File

@@ -31,7 +31,7 @@ services:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.12.0-rc3}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
@@ -52,7 +52,7 @@ services:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.12.0-rc3}
restart: unless-stopped
user: root
volumes:

4
go.mod
View File

@@ -19,7 +19,7 @@ require (
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.3
github.com/grafana/pyroscope-go v1.2.7
github.com/jackc/pgx/v5 v5.8.0
github.com/jackc/pgx/v5 v5.7.6
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
@@ -30,7 +30,7 @@ require (
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2026020.6
goauthentik.io/api/v3 v3.2025120.26
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0

8
go.sum
View File

@@ -117,8 +117,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -214,8 +214,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
goauthentik.io/api/v3 v3.2026020.6 h1:ww545OfZAS0OayLkMQGheR3AsgQ2rc61vhRwSo9dBco=
goauthentik.io/api/v3 v3.2026020.6/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
goauthentik.io/api/v3 v3.2025120.26 h1:2lTMtjCWtdOeQe7kwjpGUx39qUEpcxcxTirIqMvn0Os=
goauthentik.io/api/v3 v3.2025120.26/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=

View File

@@ -1 +1 @@
2026.2.0-rc1
2025.12.0-rc3

View File

@@ -165,7 +165,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
for _, u := range g.UsersObj {
if flag.UserPk == u.Pk {
// TODO: Is there a better way to clone this object?
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, []api.RelatedGroup{}, []api.PartialUser{u}, []api.Role{}, []string{}, []api.RelatedGroup{})
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, []api.RelatedGroup{}, []api.PartialUser{u}, []api.Role{}, nil, []string{}, []api.RelatedGroup{})
fg.SetUsers([]int32{flag.UserPk})
fg.SetAttributes(g.Attributes)
fg.SetIsSuperuser(*g.IsSuperuser)

View File

@@ -234,8 +234,8 @@ func NewPostgresStore(log *log.Entry) (*PostgresStore, error) {
}
// Determine connection pool settings
maxIdleConns := 10
maxOpenConns := 100
maxIdleConns := 4
maxOpenConns := 4
var connMaxLifetime time.Duration
if cfg.ConnMaxAge > 0 {
connMaxLifetime = time.Duration(cfg.ConnMaxAge) * time.Second

View File

@@ -28,7 +28,7 @@ func (ps *ProxyServer) Refresh() error {
return err
}
ps.log.WithField("count", len(providers)).Debug("Fetched providers")
if len(providers) == 0 && !ps.akAPI.IsEmbedded() {
if len(providers) == 0 {
ps.log.Warning("No providers assigned to this outpost, check outpost configuration in authentik")
}
for i, p := range providers {

View File

@@ -5,6 +5,7 @@ import (
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-http-utils/etag"
@@ -17,11 +18,44 @@ import (
staticWeb "goauthentik.io/web"
)
// Theme variable placeholder that can be used in file paths
// This allows for theme-specific files like logo-%(theme)s.png
const themeVariable = "%(theme)s"
// Valid themes that can be substituted for %(theme)s
var validThemes = []string{"light", "dark"}
type StorageClaims struct {
jwt.RegisteredClaims
Path string `json:"path,omitempty"`
}
// pathMatchesWithTheme checks if the requested path matches the JWT path,
// accounting for theme variable substitution.
// If the JWT path contains %(theme)s, it will match the requested path
// if substituting %(theme)s with any valid theme produces the requested path.
func pathMatchesWithTheme(jwtPath, requestedPath string) bool {
// Direct match (no theme variable)
if jwtPath == requestedPath {
return true
}
// Check if JWT path contains theme variable
if !strings.Contains(jwtPath, themeVariable) {
return false
}
// Try substituting each valid theme and check for a match
for _, theme := range validThemes {
substituted := strings.ReplaceAll(jwtPath, themeVariable, theme)
if substituted == requestedPath {
return true
}
}
return false
}
func storageTokenIsValid(usage string, r *http.Request) bool {
tokenString := r.URL.Query().Get("token")
if tokenString == "" {
@@ -51,11 +85,8 @@ func storageTokenIsValid(usage string, r *http.Request) bool {
return false
}
if claims.Path != fmt.Sprintf("%s/%s", usage, r.URL.Path) {
return false
}
return true
requestedPath := fmt.Sprintf("%s/%s", usage, r.URL.Path)
return pathMatchesWithTheme(claims.Path, requestedPath)
}
func (ws *WebServer) configureStatic() {

View File

@@ -0,0 +1,95 @@
package web
import "testing"
func TestPathMatchesWithTheme(t *testing.T) {
tests := []struct {
name string
jwtPath string
requestedPath string
want bool
}{
{
name: "exact match without theme variable",
jwtPath: "media/public/logo.png",
requestedPath: "media/public/logo.png",
want: true,
},
{
name: "no match without theme variable",
jwtPath: "media/public/logo.png",
requestedPath: "media/public/other.png",
want: false,
},
{
name: "theme variable matches light theme",
jwtPath: "media/public/logo-%(theme)s.png",
requestedPath: "media/public/logo-light.png",
want: true,
},
{
name: "theme variable matches dark theme",
jwtPath: "media/public/logo-%(theme)s.png",
requestedPath: "media/public/logo-dark.png",
want: true,
},
{
name: "theme variable does not match invalid theme",
jwtPath: "media/public/logo-%(theme)s.png",
requestedPath: "media/public/logo-blue.png",
want: false,
},
{
name: "theme variable in directory path",
jwtPath: "media/%(theme)s/logo.png",
requestedPath: "media/light/logo.png",
want: true,
},
{
name: "multiple theme variables",
jwtPath: "media/%(theme)s/logo-%(theme)s.png",
requestedPath: "media/light/logo-light.png",
want: true,
},
{
name: "multiple theme variables with dark",
jwtPath: "media/%(theme)s/logo-%(theme)s.png",
requestedPath: "media/dark/logo-dark.png",
want: true,
},
{
name: "multiple theme variables mixed themes should not match",
jwtPath: "media/%(theme)s/logo-%(theme)s.png",
requestedPath: "media/light/logo-dark.png",
want: false,
},
{
name: "theme variable with nested path",
jwtPath: "media/public/brand/logo-%(theme)s.svg",
requestedPath: "media/public/brand/logo-dark.svg",
want: true,
},
{
name: "empty paths",
jwtPath: "",
requestedPath: "",
want: true,
},
{
name: "theme variable only",
jwtPath: "%(theme)s",
requestedPath: "light",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := pathMatchesWithTheme(tt.jwtPath, tt.requestedPath)
if got != tt.want {
t.Errorf("pathMatchesWithTheme(%q, %q) = %v, want %v",
tt.jwtPath, tt.requestedPath, got, tt.want)
}
})
}
}

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
ARG TARGETOS
ARG TARGETARCH
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/ldap ./cmd/ldap
# Stage 2: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:07f41ce3f15b2bb5eb5bcd4e6efc0cb42bb7e5609e7244f636da1a91166817ca
ARG VERSION
ARG GIT_BUILD_HASH

View File

@@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "MIT",
"devDependencies": {
"aws-cdk": "^2.1100.1",
"aws-cdk": "^2.1033.0",
"cross-env": "^10.1.0"
},
"engines": {
@@ -25,9 +25,9 @@
"license": "MIT"
},
"node_modules/aws-cdk": {
"version": "2.1100.1",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1100.1.tgz",
"integrity": "sha512-q2poFrQh90TK6eqeI0zznA8r1JkDI63WVOSqC7gFGo6qjQjAnvFk/utxHoNRgAC0RL0CLd19uCcHh3jfX9NiSg==",
"version": "2.1033.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1033.0.tgz",
"integrity": "sha512-Pit2k7cVAwxoYI7RMVsOyltuy7/HGENLupJ4KAm/d8mGzOfX+SLOo9YQsx5CKY9J6ErCZ1ViLerklTfjytvQww==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -7,7 +7,7 @@
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
},
"devDependencies": {
"aws-cdk": "^2.1100.1",
"aws-cdk": "^2.1033.0",
"cross-env": "^10.1.0"
},
"engines": {

View File

@@ -18,7 +18,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2026.2.0-rc1
Default: 2025.12.0-rc3
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

Binary file not shown.

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-28 00:13+0000\n"
"POT-Creation-Date: 2025-12-12 15:51+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"
@@ -558,30 +558,6 @@ msgid ""
"encryption."
msgstr ""
#: authentik/crypto/models.py
msgid "Key algorithm type detected from the certificate's public key"
msgstr ""
#: authentik/crypto/models.py
msgid "Certificate expiry date"
msgstr ""
#: authentik/crypto/models.py
msgid "Certificate subject as RFC4514 string"
msgstr ""
#: authentik/crypto/models.py
msgid "SHA256 fingerprint of the certificate"
msgstr ""
#: authentik/crypto/models.py
msgid "SHA1 fingerprint of the certificate"
msgstr ""
#: authentik/crypto/models.py
msgid "Key ID generated from private key"
msgstr ""
#: authentik/crypto/models.py
msgid "Certificate-Key Pair"
msgstr ""
@@ -712,10 +688,6 @@ msgstr ""
msgid "Enterprise is required to create/update this object."
msgstr ""
#: authentik/enterprise/api.py
msgid "Enterprise is required to use this endpoint."
msgstr ""
#: authentik/enterprise/models.py
msgid "License"
msgstr ""

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2025.12.0-rc3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2025.12.0-rc3",
"dependencies": {
"@eslint/js": "^9.39.1",
"@goauthentik/eslint-config": "./packages/eslint-config",

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2025.12.0-rc3",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -16336,9 +16336,9 @@
}
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {

View File

@@ -17,7 +17,7 @@ COPY web .
RUN npm run build-proxy
# Stage 2: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
ARG TARGETOS
ARG TARGETARCH
@@ -47,7 +47,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/proxy ./cmd/proxy
# Stage 3: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:07f41ce3f15b2bb5eb5bcd4e6efc0cb42bb7e5609e7244f636da1a91166817ca
ARG VERSION
ARG GIT_BUILD_HASH

View File

@@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2026.2.0-rc1"
version = "2025.12.0-rc3"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*"
@@ -146,33 +146,30 @@ exclude_dirs = ["**/node_modules/**"]
[tool.codespell]
skip = [
"**/.env", # Environment files
"**/.venv", # Python virtual environment
"**/node_modules", # Node modules
"**/package-lock.json", # NPM package lock
"schema.yml", # OpenAPI schema
"./blueprints/schema.json", # Generated blueprint schema
"go.sum", # Go module file
"locale", # Django locale files
"**/web/src/locales", # Generated TypeScript locale
"**/web/xliff", # XLIFF translation files
"**/custom-elements.json", # TypeScript custom element definitions
"**/storybook-static", # Storybook build output
"**/playwright-report", # Playwright test output
"unittest.xml", # Pytest output
"./htmlcov", # Coverage HTML output
"**/out", # TypeScript type-checking output
"**/node_modules",
"**/package-lock.json",
"schema.yml",
"unittest.xml",
"./blueprints/schema.json",
"go.sum",
"locale",
"**/web/src/locales",
"**/dist", # Distributed build output
"**/storybook-static",
"**/web/xliff",
"**/playwright-report", # Playwright test output
"**/out", # TypeScript type-checking output
"./web/custom-elements.json", # TypeScript custom element definitions
"./website/build", # TODO: Remove this after moving website to docs
"./website/**/build", # TODO: Remove this after moving website to docs
"./docs/build", # Docusaurus Topic docs build output
"./docs/**/build", # Docusaurus workspaces output
"*.api.mdx", # Generated API docs
"./gen-ts-api", # Generated TypeScript API
"./gen-py-api", # Generated Python API
"./gen-go-api", # Generated Go API
"./data", # Media files
"./media", # Legacy media files
"./gen-ts-api",
"./gen-py-api",
"./gen-go-api",
"./htmlcov",
"./media",
]
dictionary = ".github/codespell-dictionary.txt,-"
ignore-words = ".github/codespell-words.txt"
@@ -287,7 +284,6 @@ module = [
"lifecycle.*",
"tests.e2e.*",
"tests.integration.*",
"tests.openid_conformance.*",
]
ignore_errors = true

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
ARG TARGETOS
ARG TARGETARCH

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:8e8f9c84609b6005af0a4a8227cee53d6226aab1c6dcb22daf5aeeb8b05480e1 AS builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.5-trixie@sha256:5d35fb8d28b9095d123b7d96095bbf3750ff18be0a87e5a21c9cffc4351fbf96 AS builder
ARG TARGETOS
ARG TARGETARCH
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/radius ./cmd/radius
# Stage 2: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:10dadf1df1337e8eb4218acef6a3027abebf0a155a95d792af736f85ab0e9588
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:07f41ce3f15b2bb5eb5bcd4e6efc0cb42bb7e5609e7244f636da1a91166817ca
ARG VERSION
ARG GIT_BUILD_HASH

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