Compare commits

..

133 Commits

Author SHA1 Message Date
authentik-automation[bot]
8d3a289d12 release: 2025.8.4 2025-09-30 00:02:15 +00:00
Jens Langhammer
99e92bf998 root: fix rustup error during build when buildcache version is outdated
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-30 01:32:01 +02:00
authentik-automation[bot]
1df7b22d29 release: 2025.8.4 2025-09-29 21:52:52 +00:00
authentik-automation[bot]
af484738f6 website/docs: 2025.8.4 release notes (cherry-pick #17119 to version-2025.8) (#17120) 2025-09-29 23:44:03 +02:00
authentik-automation[bot]
19ba3c8453 tasks: reduce default number of retries and max backoff (cherry-pick #17107 to version-2025.8) (#17109)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-29 17:33:19 +00:00
authentik-automation[bot]
9d1b384baa packages/django-dramatiq-postgres: broker: fix new messages not being picked up when too many messages are waiting (cherry-pick #17106 to version-2025.8) (#17108)
packages/django-dramatiq-postgres: broker: fix new messages not being picked up when too many messages are waiting (#17106)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-29 17:11:11 +00:00
authentik-automation[bot]
8a702b148f providers/oauth2: fix authentication error with identical app passwords (cherry-pick #17100 to version-2025.8) (#17103)
providers/oauth2: fix authentication error with identical app passwords (#17100)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-29 17:29:14 +02:00
authentik-automation[bot]
2db42a3a04 stages/identification: fix mismatched error messages (cherry-pick #17090 to version-2025.8) (#17104)
stages/identification: fix mismatched error messages (#17090)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-29 17:29:03 +02:00
authentik-automation[bot]
4403baaa28 outposts/ldap: add pwdChangeTime attribute (cherry-pick #17010 to version-2025.8) (#17101)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix ldap tests following #17010 (#17021)
2025-09-29 15:44:35 +02:00
authentik-automation[bot]
8030d4d734 cmd/server/healthcheck: info log success instead of debug (cherry-pick #17093 to version-2025.8) (#17097)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-29 15:26:48 +02:00
authentik-automation[bot]
4b7a78453c web: Fix layout class for row in LibraryPage (cherry-pick #16752 to version-2025.8) (#17091)
web: Fix layout class for 'row' in LibraryPage (#16752)

Fix layout class for 'row' in LibraryPage

Signed-off-by: Jérôme W. <jerome@wnetworks.org>
Co-authored-by: Jérôme W. <jerome@wnetworks.org>
2025-09-29 14:56:15 +02:00
authentik-automation[bot]
b9746106a0 rbac: optimize rbac assigned by users query (cherry-pick #17015 to version-2025.8) (#17092)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-29 12:47:45 +00:00
authentik-automation[bot]
faa47670ef web/admin: fix federation sources automatically selected (cherry-pick #17069 to version-2025.8) (#17070)
web/admin: fix federation sources automatically selected (#17069)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-28 20:30:19 +02:00
authentik-automation[bot]
add5f4e0cb tasks: fix logger name (cherry-pick #17009 to version-2025.8) (#17060)
* tasks: fix logger name (#17009)

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

* fix tests

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-09-27 02:33:23 +02:00
authentik-automation[bot]
7636c038d9 */bindings: order by pk (cherry-pick #17027) (#17053)
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-26 18:37:29 +02:00
authentik-automation[bot]
557f781f5c lib/config: fix listen settings (cherry-pick #17005) (#17023)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix listen settings (#17005)
2025-09-26 16:59:23 +02:00
authentik-automation[bot]
1997011328 website/docs: Update Github expression to handle non-OAuth sources gracefully (cherry-pick #17014) (#17029)
website/docs: Update Github expression to handle non-OAuth sources gracefully (#17014)

Co-authored-by: Patrick <tradiuz@gmail.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-26 14:24:10 +02:00
authentik-automation[bot]
dfdd5ebc8c lib: match exception_to_dict locals behaviour (cherry-pick #17006) (#17016)
lib: match exception_to_dict locals behaviour (#17006)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-25 17:53:36 +02:00
authentik-automation[bot]
42977caa6a core: add index on Group.is_superuser (cherry-pick #17011) (#17017)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-25 15:29:15 +00:00
authentik-automation[bot]
bce46c880d rbac: fix typo (cherry-pick #16476) (#17018)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
fix typo (#16476)
2025-09-25 15:12:03 +00:00
authentik-automation[bot]
9c987c5694 website/docs: improve discord policies when also bound to non-oauth sources (cherry-pick #17008) (#17012)
website/docs: improve discord policies when also bound to non-oauth sources (#17008)

Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-25 15:53:40 +02:00
authentik-automation[bot]
f33e50c28c webiste/docs: add missing oauth endpoints (cherry-pick #16995) (#16999)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-25 15:00:21 +02:00
authentik-automation[bot]
280d6febf7 website/docs: oauth provider: Add device and introspect to reserved slugs (cherry-pick #16994) (#16998)
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-25 14:59:56 +02:00
authentik-automation[bot]
3a32918ceb providers/ldap: add include_children parameter to cached search mode (cherry-pick #16918) (#17000)
Co-authored-by: Daniel Adu-Gyan <26229521+danieladugyan@users.noreply.github.com>
2025-09-25 12:58:24 +00:00
authentik-automation[bot]
723bd4bbbc website/developer docs: What domain for what doc version (cherry-pick #16987) (#16996)
website/developer docs: What domain for what doc version (#16987)

* website/developer docs: What domain for what doc version

Closes: AUTH-1316

* Apply suggestions from code review




---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-25 08:35:30 -04:00
authentik-automation[bot]
7b130e1832 website/docs: update url to docker-compose.yml (cherry-pick #16901) (#16986)
website/docs: update url to docker-compose.yml (#16901)

Update URL of docker-compose.yml from "https://goauthentik.io/docker-compose.yml" to "https://docs.goauthentik.io/docker-compose.yml"

Co-authored-by: Nuno Alves <nuno.alves02@gmail.com>
2025-09-24 17:41:40 -04:00
authentik-automation[bot]
3aa969d64e website/docs: add docs for source switch expression policy (cherry-pick #16878) (#16972)
website/docs: add docs for source switch expression policy (#16878)

* draft for source switch docs

* add python

* add sidebar entry

* Update website/docs/customize/policies/expression/source_switch.md




* Update website/docs/customize/policies/index.md




* Update website/docs/customize/policies/working_with_policies.md




* Update website/docs/customize/policies/working_with_policies.md




* add sectin about binding the policy

* tweak

* Update website/docs/customize/policies/index.md




* Update website/docs/customize/policies/working_with_policies.md




* Update website/docs/customize/policies/working_with_policies.md




* Update website/docs/customize/policies/working_with_policies.md




* Update website/docs/customize/policies/working_with_policies.md




* Update website/docs/customize/policies/expression.mdx




---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-24 13:24:33 +01:00
authentik-automation[bot]
04688c86b3 website/docs: random typo fixes (cherry-pick #16956) (#16959)
website/docs: random typo fixes (#16956)

Co-authored-by: Jared Harrison <50091069+Jared-Is-Coding@users.noreply.github.com>
2025-09-23 23:57:14 +02:00
authentik-automation[bot]
4f8dbb5efb website/docs: fix capitalization (cherry-pick #16944) (#16958)
website/docs: fix capitalization (#16944)

* fix capitalization

* tweak

* Update website/docs/add-secure-apps/outposts/manual-deploy-docker-compose.md




---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-23 21:15:57 +01:00
authentik-automation[bot]
3b382199eb website/docs: 2025.8: fix worker concurrency setting rename (cherry-pick #16946) (#16950)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix worker concurrency setting rename (#16946)
2025-09-23 19:52:20 +02:00
authentik-automation[bot]
db50991e9c providers/scim: fix string formatting for SCIM user filter (cherry-pick #16465) (#16852)
providers/scim: fix string formatting for SCIM user filter (#16465)

* providers/scim: fix string formatting for SCIM user filter



* format

---------

Signed-off-by: Adam Shirt <adamshirt@outlook.com>
Co-authored-by: Adam Shirt <adamshirt@outlook.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>
2025-09-17 13:48:18 +02:00
authentik-automation[bot]
88036ebe14 webiste/docs: improve user ref doc (cherry-pick #16779) (#16839)
webiste/docs: improve user ref doc (#16779)

* WIP

* WIP

* Update website/docs/users-sources/user/user_ref.mdx




* Language change

* Update website/docs/users-sources/user/user_ref.mdx




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-09-16 22:17:41 +00:00
authentik-automation[bot]
680feaefa1 release: 2025.8.3 2025-09-16 15:09:16 +00:00
authentik-automation[bot]
8676cd3a43 website/docs: 2025.8.3 release notes (cherry-pick #16809) (#16810)
website/docs: 2025.8.3 release notes (#16809)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-16 16:40:32 +02:00
authentik-automation[bot]
53e0f6b734 stages/email_authenticator: Fix email mfa loop (cherry-pick #16579) (#16807)
stages/email_authenticator: Fix email mfa loop (#16579)

* Return error message instead of infinite loop

* Remove unused code

* check for existing email device

* revert to initial behaviour

* Add test for failing when the user already has an email device

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-09-16 16:20:34 +02:00
authentik-automation[bot]
eaee475662 website/docs: update create oauth provider page (cherry-pick #16617) (#16806)
website/docs: update create oauth provider page (#16617)

* Updated the page to be more consistent with upcoming changes to the saml page

* Add note

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-16 13:33:53 +00:00
authentik-automation[bot]
19b672b3bc lifecycle: fix permission error when running worker as root (cherry-pick #16735) (#16784)
lifecycle: fix permission error when running worker as root (#16735)

* lifecycle: fix permission error when running worker as root



* fix maybe?



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-16 14:49:31 +02:00
authentik-automation[bot]
bf0a31ce86 website/docs: fix docker tabs not rendering properly (cherry-pick #16799) (#16802)
website/docs: fix docker tabs not rendering properly (#16799)

docs: fix docker tabs not rendering properly

Co-authored-by: Connor Peshek <connor@connorpeshek.me>
2025-09-16 11:00:27 +01:00
authentik-automation[bot]
28ff561400 release: 2025.8.2 2025-09-15 15:47:14 +00:00
authentik-automation[bot]
ff2472a551 website/docs: 2025.8.2 release notes (cherry-pick #16773) (#16778)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-15 17:15:44 +02:00
authentik-automation[bot]
dac302e8be sources/oauth/entra_id: do not assume group_id comes from entra (cherry-pick #16456) (#16777)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-15 16:43:34 +02:00
authentik-automation[bot]
930a6f7c6f lib/logging: only show locals when in debug mode (cherry-pick #16772) (#16774)
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-15 15:34:52 +02:00
authentik-automation[bot]
b661b0bb39 website/docs: update ssh rac doc (cherry-pick #16695) (#16720)
website/docs: update ssh rac doc (#16695)

* Added linebreak preservation and changed blocks to yaml syntax

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




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




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-12 16:05:57 +00:00
authentik-automation[bot]
5203713ca0 website/docs: re-fix sentence about Go (backport of #16736) (#16737)
* Cherry-pick #16736 to version-2025.8 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #16736
Original commit: 515a065831

* fixed conflict

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-09-12 03:33:56 -05:00
authentik-automation[bot]
94794b106e web: Docker versioning compatibility (cherry-pick #16139) (#16732)
web: Docker versioning compatibility (#16139)

* website/docs: update frontend dev docs to always use latest dev images



* website: Clarify instructions.

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
2025-09-11 14:39:12 -05:00
authentik-automation[bot]
eb8c21cf04 website/docs: dev-docs: Minimal landing + landing for dev env (cherry-pick #15246) (#16734)
website/docs: dev-docs: Minimal landing + landing for dev env (#15246)

* wip

* Update index.mdx



* Update website/docs/developer-docs/contributing.md



* Update website/docs/developer-docs/contributing.md



---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-11 14:06:48 -05:00
authentik-automation[bot]
8a501377f2 website/docs: fix typos (cherry-pick #16716) (#16719)
website/docs: fix typos (#16716)

Fix typos

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-11 14:17:33 +00:00
authentik-automation[bot]
5c67a0fecd website/docs: moves display source notes (cherry-pick #16704) (#16718)
website/docs: moves display source notes (#16704)

Moves display source note location to better location

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-11 13:37:43 +00:00
authentik-automation[bot]
145e6a3a4f website/docs: clarify docker compose install (cherry-pick #16696) (#16699)
website/docs: clarify docker compose install (#16696)

* Change order

* WIP

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-10 21:28:56 +01:00
authentik-automation[bot]
0219ed73f5 website/docs: add missing notification rule example doc to sidebar (cherry-pick #16478) (#16479)
website/docs: add missing notification rule example doc to sidebar (#16478)

Adds missing doc to sidebar

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-10 15:54:29 +00:00
authentik-automation[bot]
88ccb7857b website: Redirect Azure to Entra. Add tags for search indexing. (cherry-pick #16474) (#16483)
website: Redirect Azure to Entra. Add tags for search indexing. (#16474)

* website: Add Azure redirects, tags for  search indexing.

* website: Fix redirects tab alignment.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-09-10 14:38:57 +00:00
authentik-automation[bot]
e33d6becba website/docs: add rate limiting info to Email stage docs (cherry-pick #16668) (#16691)
website/docs: add rate limiting info to Email stage docs (#16668)

* add rate limiting info

* added Jens' edits

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




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




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




---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-10 10:35:15 +00:00
authentik-automation[bot]
41417affc0 website/docs: fix typo (cherry-pick #16681) (#16682)
website/docs: fix typo (#16681)

Fix typo

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-09 13:28:51 +00:00
authentik-automation[bot]
1b4a6c3f6d tasks: fix status and healthcheck breaking with connection issues (cherry-pick #16504) (#16673)
tasks: fix status and healthcheck breaking with connection issues (#16504)

* add high priority for inline tasks in API



* also catch psycopg errors directly



* retry locking worker state after failure



* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-08 17:04:17 -05:00
authentik-automation[bot]
aa98edd661 providers/scim: improve error message when object fails to sync (cherry-pick #16625) (#16643)
providers/scim: improve error message when object fails to sync (#16625)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-05 15:14:20 +02:00
authentik-automation[bot]
cb70331c82 providers/oauth2: add missing exp claim for logout token (cherry-pick #16593) (#16598)
providers/oauth2: add missing exp claim for logout token (#16593)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-04 00:23:20 +02:00
authentik-automation[bot]
31e105b190 web/flows: only disable login button when interactive captcha is configured and not loaded (cherry-pick #16586) (#16590)
web/flows: only disable login button when interactive captcha is configured and not loaded (#16586)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-03 18:26:33 +02:00
authentik-automation[bot]
08d2615f71 website: Update license text to use "directory" (cherry-pick #16589) (#16592)
website: Update license text to use "directory" (#16589)

To remove any potential confusion

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-03 16:08:50 +00:00
authentik-automation[bot]
bb9e8b1c42 core: bump django from 5.1.11 to v5.1.12 (cherry-pick #16584) (#16591)
core: bump django from 5.1.11 to v5.1.12 (#16584)

bump django from 5.1.11 to 5.1.12

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-09-03 17:39:33 +02:00
authentik-automation[bot]
6b8d7376a6 sources/ldap: fix malformed filter error with special characters in group DN (cherry-pick #16243) (#16585)
sources/ldap: fix malformed filter error with special characters in group DN (#16243)

* sources/ldap: fix malformed filter error with special characters in group DN

Escape special characters in LDAP group DN when constructing membership filters using ldap3's escape_filter_chars() function to prevent LDAPInvalidFilterError exceptions.

* sources/ldap: add tests for special characters in group DN filter escaping

Add comprehensive tests to verify that LDAP group DN values with special characters (parentheses, backslashes, asterisks) are properly escaped when constructing LDAP filters. Tests cover both the membership synchronization process and the escape_filter_chars function directly to ensure malformed filter errors are prevented.

* sources/ldap: fix line length for ruff linting compliance

Split long line in group filter construction to comply with 100 character limit.

* sources/ldap: move escape_filter_chars import to top-level in tests; audit other filter usages

---------

Co-authored-by: Dongwoo Jeong <dongwoo@duck.com>
Co-authored-by: Dongwoo Jeong <dongwoo.jeong@lge.com>
2025-09-03 17:20:34 +02:00
authentik-automation[bot]
4f680d8c06 root: clean up README (cherry-pick #16286) (#16573)
root: clean up README (#16286)

* wip





* Delete website/LICENSE



---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-09-03 15:49:26 +02:00
authentik-automation[bot]
3eeb741975 website: Add text copy of license (cherry-pick #16559) (#16574)
website: Add text copy of license (#16559)

* website: Add License

According to the root LICENSE, the website is licensed under Creative Commons: CC BY-SA 4.0 license. To match the root of the project and the EE dir, a text copy of the license is now included.

Updates AUTH-1246



* Update LICENSE



* Create LICENSE for proprietary logo assets

Added license information for proprietary assets.



---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-03 09:56:28 +00:00
authentik-automation[bot]
39cb638132 website/docs: remove base providers redirect. (cherry-pick #16576) (#16580)
website/docs: remove base providers redirect. (#16576)

Co-authored-by: Connor Peshek <connor@connorpeshek.me>
2025-09-03 10:26:19 +01:00
authentik-automation[bot]
be8f5d21cb website/docs: changes all -> arrows to > (cherry-pick #16569) (#16571)
website/docs: changes all -> arrows to > (#16569)

Changes all -> arrows to > as per style guide. Also fixes some bolding and capitalization

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-02 22:27:26 +00:00
authentik-automation[bot]
acf18836e8 website/docs: update external user information (cherry-pick #16493) (#16568)
website/docs: update external user information (#16493)

Updated information

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-02 14:09:52 +00:00
authentik-automation[bot]
d688621de4 website/docs: improve customize your instance page layout (cherry-pick #16367) (#16566)
website/docs: improve customize your instance page layout (#16367)

* Changes bullet points to a table. WIP

* Finished sentence and added punctuation

* Updated to match other overview/landing pages

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-02 14:27:14 +01:00
authentik-automation[bot]
5173d09191 website/docs: update how to rac and outpost landing page (cherry-pick #16442) (#16498)
website/docs: update how to rac and outpost landing page (#16442)

* Adds outpost deployment to how to rac guide and updates the outpost landing page

* Applied suggestions

* Apply suggestion

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




* Update website/docs/add-secure-apps/outposts/index.mdx



* Removed cards

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-01 08:00:48 -05:00
authentik-automation[bot]
45bab4d32f website/integrations: bitwarden: fix ent notice (cherry-pick #16502) (#16511)
website/integrations: bitwarden: fix ent notice (#16502)

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-01 08:27:39 +00:00
authentik-automation[bot]
f383e54c72 policies: remove object pk from authentik_policies_execution_time to reduce cardinality (cherry-pick #16500) (#16501)
policies: remove object pk from authentik_policies_execution_time to reduce cardinality (#16500)

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

#16386

Co-authored-by: Jens L. <jens@goauthentik.io>
2025-08-31 14:16:24 +01:00
authentik-automation[bot]
ab6b9b27cc website: Page redirect guide, documentation (backport of #16466) (#16475)
* website/docs: merge docs development environment and writing docs pages (#16402)

* Merges the docs development environment doc into the writing documentation guide to avoid duplication. Also updates the formatting of the writing docs page to make it easier to copy commands.

* Updates sidebar

* Update website/docs/developer-docs/docs/writing-documentation.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/developer-docs/docs/writing-documentation.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/developer-docs/docs/writing-documentation.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update language on -watch commands

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>

* website: Unify Netlify redirects with Docusaurus's client-side router. (backport of #16430) (#16472)

Cherry-pick #16430 to version-2025.8 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #16430
Original commit: 6d81aea5aa

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

* Cherry-pick #16466 to version-2025.8 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #16466
Original commit: 66e17fe280

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-08-29 15:13:32 +00:00
authentik-automation[bot]
db3fb0bf2e website: Unify Netlify redirects with Docusaurus's client-side router. (backport of #16430) (#16472)
Cherry-pick #16430 to version-2025.8 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #16430
Original commit: 6d81aea5aa

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-08-29 14:08:36 +00:00
authentik-automation[bot]
5859e6a5e5 core: fix client-side only validation allowing admin to set blank user password (cherry-pick #16467) (#16469)
Co-authored-by: Jens L. <jens@goauthentik.io>
fix client-side only validation allowing admin to set blank user password (#16467)
2025-08-29 15:20:46 +02:00
authentik-automation[bot]
1d3271fec7 lib/sync/outgoing: fix single object sync timeout (cherry-pick #16447) (#16453)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix single object sync timeout (#16447)
2025-08-28 19:35:33 +02:00
authentik-automation[bot]
673c8ef62c core: bump h2 from 4.2.0 to 4.3.0 (cherry-pick #16446) (#16449)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-08-28 17:14:51 +02:00
authentik-automation[bot]
4614ae320f website/docs: capitalized proper name of stages, removed old version references. (cherry-pick #16414) (#16450)
website/docs: capitalized proper name of stages, removed old version references. (#16414)

* capitalized proper name of stages, removed old version references.

* ken and dominics edits

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-28 09:52:59 -05:00
authentik-automation[bot]
10b103c0bf lib/sync: fix missing f for string (cherry-pick #16423) (#16427)
Co-authored-by: Jens L. <jens@goauthentik.io>
fix missing f for string (#16423)
2025-08-28 15:38:24 +02:00
authentik-automation[bot]
361e64a8a1 website/docs: update internal vs external user information (cherry-pick #16359) (#16421)
website/docs: update internal vs external user information (#16359)

* Update external vs internal user information

* Language updates

* Spelling

* Applied suggestion

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-27 19:45:48 +01:00
authentik-automation[bot]
e56081b863 providers/rac: fix AuthenticatedSession migration (cherry-pick #16400) (#16419)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
fix `AuthenticatedSession` migration (#16400)
2025-08-27 18:43:31 +02:00
authentik-automation[bot]
01a44b281b root: fix security.md (cherry-pick #16345) (#16415)
Co-authored-by: Dominic R <dominic@sdko.org>
fix security.md (#16345)
2025-08-27 18:15:05 +02:00
authentik-automation[bot]
7a6631c6e8 website/docs: adds details to certificates doc (cherry-pick #16335) (#16394)
website/docs: adds details to certificates doc (#16335)

* Clarifies certs directory mounting and adds instruction for manually re-triggering discovery.

* Fixed mounting info

* Update website/docs/sys-mgmt/certificates.md




* Update website/docs/sys-mgmt/certificates.md




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-08-27 16:14:45 +00:00
authentik-automation[bot]
ada973dd44 tasks/schedules: fix api search fields (cherry-pick #16405) (#16410)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix api search fields (#16405)
2025-08-27 17:04:32 +02:00
authentik-automation[bot]
3a7e962bde lifecycle: fix PROMETHEUS_MULTIPROC_DIR missing suffix (cherry-pick #16401) (#16407)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix PROMETHEUS_MULTIPROC_DIR missing suffix (#16401)
2025-08-27 15:32:23 +02:00
authentik-automation[bot]
ae297e2f60 root: update security.md with github reporting link (cherry-pick #16332) (#16395)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-27 14:31:06 +02:00
Teffen Ellis
2bc2b6bd41 website: cherry-pick 2025.8/client side redirects (#16372)
website: Fix stale links that depend on initial route not triggering redirects. (#16369)

website: Fix issue where stale links that depend on initial route are
not redirected.
2025-08-26 14:36:58 -05:00
Marc 'risson' Schmitt
8199371172 providers/oauth2: avoid deadlock during session migration (cherry-pick #16361) (#16364) 2025-08-25 18:29:53 +02:00
Marc 'risson' Schmitt
dac1879de5 website/docs: 2025.8.1 release notes (cherry-pick #16343) (#16344) 2025-08-22 16:53:58 +02:00
authentik-automation[bot]
dd7c6b29d9 release: 2025.8.1 2025-08-22 14:40:58 +00:00
Marc 'risson' Schmitt
41b7e05f59 packages/django-dramatiq-postgres: broker: fix various timing issues (cherry-pick #16340) (#16342)
fix various timing issues (#16340)
2025-08-22 16:08:53 +02:00
Teffen Ellis
c5f5714e02 website: Hotfix version 2025.8/website versioning (#16336)
Co-authored-by: Dominic R <dominic@sdko.org>
Fix version origin detection, build-time URLs  (#15774)
2025-08-22 16:08:34 +02:00
Marc 'risson' Schmitt
d20d8322af outposts: allow ingress path type configuration (cherry-pick #16339) (#16341) 2025-08-22 15:49:33 +02:00
Marc 'risson' Schmitt
288f5d5015 outposts: fix service connection update task arguments (cherry-pick #16312) (#16313) 2025-08-22 14:31:56 +02:00
Marc 'risson' Schmitt
a640eb9180 packages/django-dramatiq-postgres: middleware: fix listening on hosts where ipv6 is not supported (cherry-pick #16308) (#16305) 2025-08-22 14:26:49 +02:00
Marc 'risson' Schmitt
7d48baab3e core: use email backend for test_email management command (cherry-pick #16311) (#16337)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-08-22 14:25:06 +02:00
Tana M Berry
b1cd6d34fc website/docs: add link in 2025.8 rel notes to back-channel logout doc (cherry pick #16306) (#16318)
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-21 22:35:36 +02:00
Teffen Ellis
f14d033cef web: Username truncation, field alignment. (cherry-pick #16283) (#16284) 2025-08-21 18:03:55 +02:00
Teffen Ellis
ec75e161e2 web: Fix form group slots (cherry-pick #16276) (#16277) 2025-08-21 17:56:36 +02:00
Teffen Ellis
2e8fb8f2c6 web: Improvements to ReCaptcha resizing (cherry-pick #16171) (#16281) 2025-08-21 17:56:11 +02:00
Teffen Ellis
362bf22139 web: Fix issue where clicking a list item scrolls container (cherry-pick #16174) (#16278) 2025-08-21 17:49:43 +02:00
Marc 'risson' Schmitt
890da9b287 lifecycle: set PROMETHEUS_MULTIPROC_DIR as early as possible (cherry-pick #16298) (#16302) 2025-08-21 16:48:50 +02:00
Marc 'risson' Schmitt
7b8dadf945 providers/oauth2: fix logout token missing sid, fix wrong sub mode used (cherry-pick #16295) (#16299)
fix logout token missing sid, fix wrong sub mode used (#16295)
2025-08-21 16:39:18 +02:00
Teffen Ellis
8f70dbb963 web: Fix hidden textarea required attribute. (cherry-pick #16168) (#16275)
Fix hidden textarea `required` attribute. (#16168)
2025-08-21 14:50:36 +02:00
Teffen Ellis
15505f5caf web: fix "Explore integrations" link in Quick actions (cherry-pick #16274) (#16285)
Co-authored-by: Max <mail@heavygale.de>
fix "Explore integrations" link in Quick actions (#16274)
2025-08-21 14:20:07 +02:00
Marc 'risson' Schmitt
b2d770c0a4 *: Fix dead doc link (cherry-pick #16288) (#16297)
Co-authored-by: Dominic R <dominic@sdko.org>
Fix dead doc link (#16288)
2025-08-21 14:10:33 +02:00
authentik-automation[bot]
f7e25fff1a release: 2025.8.0 2025-08-20 18:01:34 +00:00
Marc 'risson' Schmitt
688b3a9f8b tasks: add rel_obj to system task exception event (cherry-pick #16270) (#16272) 2025-08-20 19:31:02 +02:00
Marc 'risson' Schmitt
8f48e18854 website/docs: update 2025.8 release notes (cherry-pick #16269) (#16271) 2025-08-20 19:20:18 +02:00
Marc 'risson' Schmitt
306f75be59 security: Bump supported versions (cherry-pick #16261) (#16267)
Co-authored-by: Dominic R <dominic@sdko.org>
2025-08-20 19:17:08 +02:00
Tana M Berry
982c3cf4dc website/docs: sys-mgmt/s3: Clean up and improve (cherry-pick #16242) (#16266)
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-20 13:24:51 +02:00
Tana M Berry
156cda6cb6 website/docs: update Advanced Queries docs and Rel Notes with new Int Guide (cherry-pick #16191) (#16258)
website/docs: Advanced queries, remove reference to QL and add more examples (#16191)

* remove reference to QL

* add Jens' examples

* tweak

* Update website/docs/users-sources/user/user_basic_operations.md




* Update website/docs/users-sources/user/user_basic_operations.md




* add note about UX ticks

* tweak

* argh

* clarify there are more values

* add link to Event actions list

* tweaks, typo

* Update website/docs/users-sources/user/user_basic_operations.md




* Update website/docs/sys-mgmt/events/logging-events.md




* jens edits

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-20 04:36:47 -05:00
authentik-automation[bot]
9f5125cf6b release: 2025.8.0-rc7 2025-08-18 18:44:42 +00:00
Marc 'risson' Schmitt
a84411363e web: Fix ak-flow-card footer alignment. (cherry-pick #16236) (#16238)
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Fix ak-flow-card footer alignment. (#16236)
2025-08-18 20:15:21 +02:00
Marc 'risson' Schmitt
9f3bb0210b brands: revert sort matched brand by match length (revert #15413) (cherry-pick #16233) (#16235) 2025-08-18 19:28:19 +02:00
Marc 'risson' Schmitt
d1271502ef web: bump API Client version (cherry-pick #16203) (#16226)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-08-18 16:42:35 +02:00
Marc 'risson' Schmitt
d1065b2d49 policies/password: Fix amount_uppercase in password policy check (cherry-pick #16197) (#16228)
Co-authored-by: M-Slanec <mcslanec@gmail.com>
Co-authored-by: Matthew Slanec <matthewslanec@Matthews-MacBook-Pro.local>
Fix amount_uppercase in password policy check (#16197)
2025-08-18 16:42:26 +02:00
Marc 'risson' Schmitt
aa19227e30 web/a11y: QL Search Input (cherry-pick #16198) (#16229)
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-08-18 16:41:50 +02:00
Marc 'risson' Schmitt
d7a2861bbe stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (cherry-pick #16196) (#16227)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-08-18 16:41:33 +02:00
Marc 'risson' Schmitt
5aae9c9afa core: Add email template selector (cherry-pick #16170) (#16225)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-08-18 16:41:22 +02:00
Tana M Berry
93cb48c928 website/docs: add content about new Advanced Query searches (cherry-pick #16019) (#16188)
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-14 18:32:05 +02:00
Marc 'risson' Schmitt
55f7f93a24 web/admin: fix settings saving (cherry-pick #16184) (#16187)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-08-14 13:02:30 +00:00
authentik-automation[bot]
5a608a4235 release: 2025.8.0-rc6 2025-08-14 11:29:06 +00:00
Marc 'risson' Schmitt
77d023758f tasks: add sentry dramatiq integration (cherry-pick #16167) (#16183)
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-08-14 13:05:59 +02:00
Marc 'risson' Schmitt
f9edafd374 ci: release tag: fix missing env variables (cherry-pick #16172) (#16173) 2025-08-13 21:31:01 +02:00
Marc 'risson' Schmitt
d94219eb0e ci: release: consolidation bump version and on tag (cherry-pick #16164) (#16169)
Co-authored-by: Dominic R <dominic@sdko.org>
2025-08-13 18:22:11 +02:00
Marc 'risson' Schmitt
0871aa2cf3 ci: fix docker hub credentials (cherry-pick #16165) (#16166) 2025-08-13 15:20:03 +02:00
authentik-automation[bot]
e0fe99d0b8 release: 2025.8.0-rc5 2025-08-13 00:21:34 +00:00
Marc 'risson' Schmitt
c1acf53585 root: fix custom packages installation in docker (cherry-pick #16157) (#16158) 2025-08-13 02:08:56 +02:00
authentik-automation[bot]
baf4eed0d9 release: 2025.8.0-rc4 2025-08-12 22:26:57 +00:00
Marc 'risson' Schmitt
60e1192a7a ci: release publish: fix missing permissions (cherry-pick #16155) (#16156) 2025-08-13 00:20:52 +02:00
authentik-automation[bot]
2608e02d6e release: 2025.8.0-rc3 2025-08-12 21:49:51 +00:00
Marc 'risson' Schmitt
0a5928fbcb ci: docker push: fix version missing dash (cherry-pick #16153) (#16154) 2025-08-12 23:44:01 +02:00
authentik-automation[bot]
7b99b02b4a release: 2025.8.0-rc2 2025-08-12 21:12:07 +00:00
Marc 'risson' Schmitt
dfeadc9ebe root: fix custom packages installation in docker (cherry-pick #16150) (#16151) 2025-08-12 23:08:40 +02:00
authentik-automation[bot]
7ff64fbd09 release: 2025.8.0-rc1 2025-08-12 20:31:40 +00:00
922 changed files with 27550 additions and 34508 deletions

View File

@@ -1,267 +0,0 @@
name: "Cherry-picker"
description: "Cherry-pick PRs based on their labels"
inputs:
token:
description: "GitHub Token"
required: true
git_user:
description: "Git user for pushing the cherry-pick PR"
required: true
git_user_email:
description: "Git user email for pushing the cherry-pick PR"
required: true
runs:
using: "composite"
steps:
- name: Check if workflow should run
id: should_run
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
run: |
set -e -o pipefail
# For issues events, check if it's actually a PR
if [ "${{ github.event_name }}" = "issues" ]; then
# Check if this issue is actually a PR
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} 2>/dev/null || echo "null")
if [ "$PR_DATA" = "null" ]; then
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=not_a_pr" >> $GITHUB_OUTPUT
echo "This is an issue, not a PR. Skipping."
exit 0
fi
# Get PR data
PR_MERGED=$(echo "$PR_DATA" | jq -r '.merged')
PR_NUMBER="${{ github.event.issue.number }}"
MERGE_COMMIT_SHA=$(echo "$PR_DATA" | jq -r '.merge_commit_sha')
# Check if it's a backport label
LABEL_NAME="${{ github.event.label.name }}"
if [[ "$LABEL_NAME" =~ ^backport/(.+)$ ]]; then
if [ "$PR_MERGED" = "true" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "reason=label_added_to_merged_pr" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "merge_commit_sha=$MERGE_COMMIT_SHA" >> $GITHUB_OUTPUT
exit 0
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=label_added_to_open_pr" >> $GITHUB_OUTPUT
echo "Backport label added to open PR. Will run after PR is merged."
exit 0
fi
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=non_backport_label" >> $GITHUB_OUTPUT
exit 0
fi
fi
# For pull_request and pull_request_target events
PR_NUMBER="${{ github.event.pull_request.number }}"
MERGE_COMMIT_SHA="${{ github.event.pull_request.merge_commit_sha }}"
# Case 1: PR was just merged (closed + merged = true)
if [ "${{ github.event.action }}" = "closed" ] && [ "${{ github.event.pull_request.merged }}" = "true" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "reason=pr_merged" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "merge_commit_sha=$MERGE_COMMIT_SHA" >> $GITHUB_OUTPUT
exit 0
fi
# Case 2: Label was added
if [ "${{ github.event.action }}" = "labeled" ]; then
LABEL_NAME="${{ github.event.label.name }}"
# Check if it's a backport label
if [[ "$LABEL_NAME" =~ ^backport/(.+)$ ]]; then
# Check if PR is already merged
if [ "${{ github.event.pull_request.merged }}" = "true" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "reason=label_added_to_merged_pr" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "merge_commit_sha=$MERGE_COMMIT_SHA" >> $GITHUB_OUTPUT
exit 0
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=label_added_to_open_pr" >> $GITHUB_OUTPUT
echo "Backport label added to open PR. Will run after PR is merged."
exit 0
fi
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=non_backport_label" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=unknown" >> $GITHUB_OUTPUT
- name: Configure Git
if: steps.should_run.outputs.should_run == 'true'
shell: bash
env:
user: ${{ inputs.git_user }}
email: ${{ inputs.git_user_email }}
run: |
git config --global user.name "${user}"
git config --global user.email "${email}"
- name: Get PR details and extract backport labels
if: steps.should_run.outputs.should_run == 'true'
id: pr_details
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
run: |
set -e -o pipefail
PR_NUMBER="${{ steps.should_run.outputs.pr_number }}"
# Get PR details
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.user.login')
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT
# Determine which labels to process
if [ "${{ steps.should_run.outputs.reason }}" = "label_added_to_merged_pr" ]; then
# Only process the specific label that was just added
if [ "${{ github.event_name }}" = "issues" ]; then
LABEL_NAME="${{ github.event.label.name }}"
else
LABEL_NAME="${{ github.event.label.name }}"
fi
if [[ "$LABEL_NAME" =~ ^backport/(.+)$ ]]; then
echo "labels=$LABEL_NAME" >> $GITHUB_OUTPUT
else
echo "Label $LABEL_NAME does not match backport pattern"
echo "labels=" >> $GITHUB_OUTPUT
fi
else
# PR was just merged, process all backport labels
LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name' | grep '^backport/' | tr '\n' ' ' || true)
echo "labels=$LABELS" >> $GITHUB_OUTPUT
fi
- name: Cherry-pick to target branches
if: steps.should_run.outputs.should_run == 'true' && steps.pr_details.outputs.labels != ''
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
run: |
set -e -o pipefail
PR_NUMBER='${{ steps.should_run.outputs.pr_number }}'
COMMIT_SHA='${{ steps.should_run.outputs.merge_commit_sha }}'
PR_TITLE='${{ steps.pr_details.outputs.pr_title }}'
PR_AUTHOR='${{ steps.pr_details.outputs.pr_author }}'
LABELS='${{ steps.pr_details.outputs.labels }}'
echo "Processing PR #$PR_NUMBER (reason: ${{ steps.should_run.outputs.reason }})"
echo "Found backport labels: $LABELS"
# Process each backport label
for label in $LABELS; do
if [[ "$label" =~ ^backport/(.+)$ ]]; then
TARGET_BRANCH="${BASH_REMATCH[1]}"
echo "Processing backport to branch: $TARGET_BRANCH"
# Check if target branch exists
if ! git ls-remote --heads origin "$TARGET_BRANCH" | grep -q "$TARGET_BRANCH"; then
echo "❌ Target branch $TARGET_BRANCH does not exist, skipping"
# Comment on the original PR about the missing branch
gh pr comment $PR_NUMBER --body "⚠️ Cannot backport to \`$TARGET_BRANCH\`: branch does not exist."
continue
fi
# Create a unique branch name for the cherry-pick
CHERRY_PICK_BRANCH="cherry-pick/${PR_NUMBER}-to-${TARGET_BRANCH}"
# Check if a cherry-pick PR already exists
EXISTING_PR=$(gh pr list --head "$CHERRY_PICK_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING_PR" ]; then
echo "⚠️ Cherry-pick PR already exists: #$EXISTING_PR"
gh pr comment $PR_NUMBER --body "Cherry-pick to \`$TARGET_BRANCH\` already exists: #$EXISTING_PR"
continue
fi
# Fetch and checkout target branch
git fetch origin "$TARGET_BRANCH"
git checkout -b "$CHERRY_PICK_BRANCH" "origin/$TARGET_BRANCH"
# Attempt cherry-pick
if git cherry-pick "$COMMIT_SHA"; then
echo "✅ Cherry-pick successful for $TARGET_BRANCH"
# Push the cherry-pick branch
git push origin "$CHERRY_PICK_BRANCH"
# Create PR for the cherry-pick
CHERRY_PICK_TITLE="$PR_TITLE (cherry-pick #$PR_NUMBER to $TARGET_BRANCH)"
CHERRY_PICK_BODY="Cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\` branch.
**Original PR:** #$PR_NUMBER
**Original Author:** @$PR_AUTHOR
**Cherry-picked commit:** $COMMIT_SHA"
NEW_PR=$(gh pr create \
--title "$CHERRY_PICK_TITLE" \
--body "$CHERRY_PICK_BODY" \
--base "$TARGET_BRANCH" \
--head "$CHERRY_PICK_BRANCH" \
--label "cherry-pick")
echo "✅ Created cherry-pick PR $NEW_PR for $TARGET_BRANCH"
# Comment on original PR
gh pr comment $PR_NUMBER --body "🍒 Cherry-pick to \`$TARGET_BRANCH\` created: $NEW_PR"
else
echo "⚠️ Cherry-pick failed for $TARGET_BRANCH, creating conflict resolution PR"
# Add conflicted files and commit
git add .
git commit -m "Cherry-pick #$PR_NUMBER to $TARGET_BRANCH (with conflicts)
This cherry-pick has conflicts that need manual resolution.
Original PR: #$PR_NUMBER
Original commit: $COMMIT_SHA"
# Push the branch with conflicts
git push origin "$CHERRY_PICK_BRANCH"
# Create PR with conflict notice
CONFLICT_TITLE="$PR_TITLE (cherry-pick #$PR_NUMBER to $TARGET_BRANCH)"
CONFLICT_BODY="⚠️ **This cherry-pick has conflicts that require manual resolution.**
Cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\` branch.
**Original PR:** #$PR_NUMBER
**Original Author:** @$PR_AUTHOR
**Cherry-picked commit:** $COMMIT_SHA
**Please resolve the conflicts in this PR before merging.**"
NEW_PR=$(gh pr create \
--title "$CONFLICT_TITLE" \
--body "$CONFLICT_BODY" \
--base "$TARGET_BRANCH" \
--head "$CHERRY_PICK_BRANCH" \
--label "cherry-pick")
echo "⚠️ Created conflict resolution PR $NEW_PR for $TARGET_BRANCH"
# Comment on original PR
gh pr comment $PR_NUMBER --body "⚠️ Cherry-pick to \`$TARGET_BRANCH\` has conflicts: $NEW_PR"
fi
# Clean up - go back to main branch
git checkout main
git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true
fi
done

View File

@@ -2,28 +2,16 @@
import os
from json import dumps
from sys import exit as sysexit
from time import time
from authentik import authentik_version
def must_or_fail(input: str | None, error: str) -> str:
if not input:
print(f"::error::{error}")
sysexit(1)
return input
# Decide if we should push the image or not
should_push = True
if len(os.environ.get("DOCKER_USERNAME", "")) < 1:
# Don't push if we don't have DOCKER_USERNAME, i.e. no secrets are available
should_push = False
if (
must_or_fail(os.environ.get("GITHUB_REPOSITORY"), "Repo required").lower()
== "goauthentik/authentik-internal"
):
if os.environ.get("GITHUB_REPOSITORY").lower() == "goauthentik/authentik-internal":
# Don't push on the internal repo
should_push = False
@@ -32,16 +20,13 @@ if os.environ.get("GITHUB_HEAD_REF", "") != "":
branch_name = os.environ["GITHUB_HEAD_REF"]
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-").replace("'", "-")
image_names = must_or_fail(os.getenv("IMAGE_NAME"), "Image name required").split(",")
image_names = os.getenv("IMAGE_NAME").split(",")
image_arch = os.getenv("IMAGE_ARCH") or None
is_pull_request = bool(os.getenv("PR_HEAD_SHA"))
is_release = "dev" not in image_names[0]
sha = must_or_fail(
os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA"),
"could not determine SHA",
)
sha = os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA")
# 2042.1.0 or 2042.1.0-rc1
version = authentik_version()
@@ -73,7 +58,7 @@ else:
image_main_tag = image_tags[0].split(":")[-1]
def get_attest_image_names(image_with_tags: list[str]) -> str:
def get_attest_image_names(image_with_tags: list[str]):
"""Attestation only for GHCR"""
image_tags = []
for image_name in set(name.split(":")[0] for name in image_with_tags):
@@ -97,6 +82,7 @@ if os.getenv("RELEASE", "false").lower() == "true":
image_build_args = [f"VERSION={os.getenv('REF')}"]
else:
image_build_args = [f"GIT_BUILD_HASH={sha}"]
image_build_args = "\n".join(image_build_args)
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"shouldPush={str(should_push).lower()}", file=_output)
@@ -109,4 +95,4 @@ with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"imageMainTag={image_main_tag}", file=_output)
print(f"imageMainName={image_tags[0]}", file=_output)
print(f"cacheTo={cache_to}", file=_output)
print(f"imageBuildArgs={"\n".join(image_build_args)}", file=_output)
print(f"imageBuildArgs={image_build_args}", file=_output)

2
.github/cherry-pick-bot.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
enabled: true
preservePullRequestTitle: true

View File

@@ -77,12 +77,6 @@ updates:
goauthentik:
patterns:
- "@goauthentik/*"
react:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
- package-ecosystem: npm
directory: "/website"
schedule:

View File

@@ -72,13 +72,6 @@ jobs:
run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: Setup node
if: ${{ !inputs.release }}
uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: generate ts client
if: ${{ !inputs.release }}
run: make gen-client-ts
@@ -97,7 +90,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@v3
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@@ -21,7 +21,7 @@ on:
jobs:
build-server-amd64:
uses: ./.github/workflows/_reusable-docker-build-single.yml
uses: ./.github/workflows/_reusable-docker-build-single.yaml
secrets: inherit
with:
image_name: ${{ inputs.image_name }}
@@ -31,7 +31,7 @@ jobs:
registry_ghcr: ${{ inputs.registry_ghcr }}
release: ${{ inputs.release }}
build-server-arm64:
uses: ./.github/workflows/_reusable-docker-build-single.yml
uses: ./.github/workflows/_reusable-docker-build-single.yaml
secrets: inherit
with:
image_name: ${{ inputs.image_name }}
@@ -97,7 +97,7 @@ jobs:
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@v3
- uses: actions/attest-build-provenance@v2
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}

68
.github/workflows/api-py-publish.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
---
name: API - Publish Python client
on:
push:
branches: [main]
paths:
- "schema.yml"
workflow_dispatch:
jobs:
build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Install poetry & deps
shell: bash
run: |
pipx install poetry || true
sudo apt-get update
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
- name: Setup python and restore poetry
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"
- name: Generate API Client
run: make gen-client-py
- name: Publish package
working-directory: gen-py-api/
run: |
poetry build
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: gen-py-api/dist/
# We can't easily upgrade the API client being used due to poetry being poetry
# so we'll have to rely on dependabot
# - name: Upgrade /
# run: |
# export VERSION=$(cd gen-py-api && poetry version -s)
# poetry add "authentik_client=$VERSION" --allow-prereleases --lock
# - uses: peter-evans/create-pull-request@v6
# id: cpr
# with:
# token: ${{ steps.generate_token.outputs.token }}
# branch: update-root-api-client
# commit-message: "root: bump API Client version"
# title: "root: bump API Client version"
# body: "root: bump API Client version"
# delete-branch: true
# signoff: true
# # ID from https://api.github.com/users/authentik-automation[bot]
# author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
# - uses: peter-evans/enable-pull-request-automerge@v3
# with:
# token: ${{ steps.generate_token.outputs.token }}
# pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
# merge-method: squash

View File

@@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v5
with:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
registry-url: "https://registry.npmjs.org"

View File

@@ -33,7 +33,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
@@ -71,7 +71,7 @@ jobs:
with:
name: api-docs
path: website/api/build
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"

View File

@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: lifecycle/aws/package.json
cache: "npm"

View File

@@ -33,7 +33,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
@@ -49,7 +49,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
@@ -102,7 +102,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@v3
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@@ -34,7 +34,6 @@ jobs:
- codespell
- pending-migrations
- ruff
- mypy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -257,7 +256,7 @@ jobs:
# Needed for checkout
contents: read
needs: ci-core-mark
uses: ./.github/workflows/_reusable-docker-build.yml
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
with:
image_name: ${{ github.repository == 'goauthentik/authentik-internal' && 'ghcr.io/goauthentik/internal-server' || 'ghcr.io/goauthentik/dev-server' }}

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Prepare and generate API
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Setup authentik env
@@ -115,7 +115,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@v3
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:
@@ -141,10 +141,10 @@ jobs:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-go@v6
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"

View File

@@ -32,7 +32,7 @@ jobs:
project: web
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
@@ -77,7 +77,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"

View File

@@ -1,36 +0,0 @@
name: GH - Cherry-pick
on:
pull_request_target:
types: [closed, labeled]
jobs:
cherry-pick:
runs-on: ubuntu-latest
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@v2
if: ${{ env.GH_APP_ID != '' }}
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
env:
GH_APP_ID: ${{ secrets.GH_APP_ID }}
- uses: actions/checkout@v5
if: ${{ steps.app-token.outcome != 'skipped' }}
with:
fetch-depth: 0
token: "${{ steps.app-token.outputs.token }}"
- id: get-user-id
if: ${{ steps.app-token.outcome != 'skipped' }}
name: Get GitHub app user ID
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: ./.github/actions/cherry-pick
if: ${{ steps.app-token.outcome != 'skipped' }}
with:
token: ${{ steps.app-token.outputs.token }}
git_user: ${{ steps.app-token.outputs.app-slug }}[bot]
git_user_email: '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'

View File

@@ -29,13 +29,13 @@ jobs:
- uses: actions/checkout@v5
with:
fetch-depth: 2
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: ${{ matrix.package }}/package.json
registry-url: "https://registry.npmjs.org"
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c
with:
files: |
${{ matrix.package }}/package.json

View File

@@ -43,13 +43,10 @@ jobs:
with:
dependencies: python
- name: Create version branch
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
run: |
current_major_version="$(uv version --short | grep -oE "^[0-9]{4}\.[0-9]{1,2}")"
git checkout -b "version-${current_major_version}"
git push origin "version-${current_major_version}"
gh label create "backport/version-${current_major_version}" --description "Add this label to PRs to backport changes to version-${current_major_version}" --color "fbca04"
bump-version-pr:
name: Open version bump PR
needs:

View File

@@ -7,7 +7,7 @@ on:
jobs:
build-server:
uses: ./.github/workflows/_reusable-docker-build.yml
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
permissions:
contents: read
@@ -58,7 +58,7 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@v3
- uses: actions/attest-build-provenance@v2
id: attest
if: true
with:
@@ -84,7 +84,7 @@ jobs:
- rac
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Set up QEMU
@@ -124,7 +124,7 @@ jobs:
file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@v3
- uses: actions/attest-build-provenance@v2
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
@@ -147,10 +147,10 @@ jobs:
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- uses: actions/setup-node@v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
@@ -187,7 +187,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: aws-actions/configure-aws-credentials@v5
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
aws-region: ${{ env.AWS_REGION }}

View File

@@ -20,7 +20,7 @@ jobs:
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/stale@v10
- uses: actions/stale@v9
with:
repo-token: ${{ steps.generate_token.outputs.token }}
days-before-stale: 60

12
.vscode/settings.json vendored
View File

@@ -1,16 +1,4 @@
{
"[css]": {
"editor.minimap.markSectionHeaderRegex": "#\\bregion\\s*(?<separator>-?)\\s*(?<label>.*)\\*/$"
},
"[makefile]": {
"editor.minimap.markSectionHeaderRegex": "^#{25}\n##\\s\\s*(?<separator>-?)\\s*(?<label>[^\n]*)\n#{25}$"
},
"[dockerfile]": {
"editor.minimap.markSectionHeaderRegex": "\\bStage\\s*\\d:(?<separator>-?)\\s*(?<label>.*)$"
},
"[jsonc]": {
"editor.minimap.markSectionHeaderRegex": "#\\bregion\\s*(?<separator>-?)\\s*(?<label>.*)$"
},
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"yaml.customTags": [

View File

@@ -33,12 +33,17 @@ packages/prettier-config @goauthentik/frontend
packages/tsconfig @goauthentik/frontend
# Web
web/ @goauthentik/frontend
tests/wdio/ @goauthentik/frontend
# Locale
locale/ @goauthentik/backend @goauthentik/frontend
web/xliff/ @goauthentik/backend @goauthentik/frontend
# Docs
# Docs & Website
docs/ @goauthentik/docs
# TODO Remove after moving website to docs
website/ @goauthentik/docs
CODE_OF_CONDUCT.md @goauthentik/docs
# Security
SECURITY.md @goauthentik/security @goauthentik/docs
# TODO Remove after moving website to docs
website/security/ @goauthentik/security @goauthentik/docs
docs/security/ @goauthentik/security @goauthentik/docs

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-bookworm AS go-builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
ARG TARGETOS
ARG TARGETARCH
@@ -76,9 +76,9 @@ 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.8.22 AS uv
FROM ghcr.io/astral-sh/uv:0.8.8 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.7-slim-trixie-fips AS python-base
FROM ghcr.io/goauthentik/fips-python:3.13.6-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
@@ -119,7 +119,11 @@ RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/v
libltdl-dev && \
curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec" \
# https://github.com/rust-lang/rustup/issues/2949
# Fixes issues where the rust version in the build cache is older than latest
# and rustup tries to update it, which fails
RUSTUP_PERMIT_COPY_RENAME="true"
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=bind,target=uv.lock,src=uv.lock \

View File

@@ -18,24 +18,7 @@ pg_host := $(shell uv run python -m authentik.lib.config postgresql.host 2>/dev/
pg_name := $(shell uv run python -m authentik.lib.config postgresql.name 2>/dev/null)
redis_db := $(shell uv run python -m authentik.lib.config redis.db 2>/dev/null)
UNAME := $(shell uname)
# For macOS users, add the libxml2 installed from brew libxmlsec1 to the build path
# to prevent SAML-related tests from failing and ensure correct pip dependency compilation
ifeq ($(UNAME), Darwin)
# Only add for brew users who installed libxmlsec1
BREW_EXISTS := $(shell command -v brew 2> /dev/null)
ifdef BREW_EXISTS
LIBXML2_EXISTS := $(shell brew list libxml2 2> /dev/null)
ifdef LIBXML2_EXISTS
BREW_LDFLAGS := -L$(shell brew --prefix libxml2)/lib $(LDFLAGS)
BREW_CPPFLAGS := -I$(shell brew --prefix libxml2)/include $(CPPFLAGS)
BREW_PKG_CONFIG_PATH := $(shell brew --prefix libxml2)/lib/pkgconfig:$(PKG_CONFIG_PATH)
endif
endif
endif
all: lint-fix lint gen web test ## Lint, build, and test everything
all: lint-fix lint test gen web ## Lint, build, and test everything
HELP_WIDTH := $(shell grep -h '^[a-z][^ ]*:.*\#\#' $(MAKEFILE_LIST) 2>/dev/null | \
cut -d':' -f1 | awk '{printf "%d\n", length}' | sort -rn | head -1)
@@ -67,14 +50,7 @@ lint: ## Lint the python and golang sources
golangci-lint run -v
core-install:
ifdef LIBXML2_EXISTS
# Clear cache to ensure fresh compilation
uv cache clean
# Force compilation from source for lxml and xmlsec with correct environment
LDFLAGS="$(BREW_LDFLAGS)" CPPFLAGS="$(BREW_CPPFLAGS)" PKG_CONFIG_PATH="$(BREW_PKG_CONFIG_PATH)" uv sync --frozen --reinstall-package lxml --reinstall-package xmlsec --no-binary-package lxml --no-binary-package xmlsec
else
uv sync --frozen
endif
migrate: ## Run the Authentik Django server's migrations
uv run python -m lifecycle.migrate
@@ -184,7 +160,7 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.15.0 generate \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \
-g typescript-fetch \
-o /local/${GEN_API_TS} \
@@ -193,7 +169,6 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
--git-repo-id authentik \
--git-user-id goauthentik
cd ${PWD}/${GEN_API_TS} && npm i
cd ${PWD}/${GEN_API_TS} && npm link
cd ${PWD}/web && npm link @goauthentik/api
@@ -201,7 +176,7 @@ gen-client-py: gen-clean-py ## Build and install the authentik API for Python
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.15.0 generate \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \
-g python \
-o /local/${GEN_API_PY} \
@@ -239,30 +214,34 @@ node-install: ## Install the necessary libraries to build Node.js packages
#########################
web-build: node-install ## Build the Authentik UI
npm run --prefix web build
cd web && npm run build
web: web-lint-fix web-lint web-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
web-test: ## Run tests for the Authentik UI
npm run --prefix web test
cd web && npm run test
web-watch: ## Build and watch the Authentik UI for changes, updating automatically
npm run --prefix web watch
rm -rf web/dist/
mkdir web/dist/
touch web/dist/.gitkeep
cd web && npm run watch
web-storybook-watch: ## Build and run the storybook documentation server
npm run --prefix web storybook
cd web && npm run storybook
web-lint-fix:
npm run --prefix web prettier
cd web && npm run prettier
web-lint:
npm run --prefix web lint
npm run --prefix web lit-analyse
cd web && npm run lint
cd web && npm run lit-analyse
web-check-compile:
npm run --prefix web tsc
cd web && npm run tsc
web-i18n-extract:
npm run --prefix web extract-locales
cd web && npm run extract-locales
#########################
## Docs
@@ -274,31 +253,31 @@ docs-install:
npm ci --prefix website
docs-lint-fix: lint-codespell
npm run --prefix website prettier
npm run prettier --prefix website
docs-build:
npm run --prefix website build
npm run build --prefix website
docs-watch: ## Build and watch the topics documentation
npm run --prefix website start
npm run start --prefix website
integrations: docs-lint-fix integrations-build ## Fix formatting issues in the integrations source code, lint the code, and compile it
integrations-build:
npm run --prefix website -w integrations build
npm run build --prefix website -w integrations
integrations-watch: ## Build and watch the Integrations documentation
npm run --prefix website -w integrations start
npm run start --prefix website -w integrations
docs-api-build:
npm run --prefix website -w api build
npm run build --prefix website -w api
docs-api-watch: ## Build and watch the API documentation
npm run --prefix website -w api build:api
npm run --prefix website -w api start
npm run build:api --prefix website -w api
npm run start --prefix website -w api
docs-api-clean: ## Clean generated API documentation
npm run --prefix website -w api build:api:clean
npm run build:api:clean --prefix website -w api
#########################
## Docker
@@ -321,9 +300,6 @@ ci--meta-debug:
python -V
node --version
ci-mypy: ci--meta-debug
uv run mypy --strict $(PY_SOURCES)
ci-black: ci--meta-debug
uv run black --check $(PY_SOURCES)

View File

@@ -9,6 +9,7 @@
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/goauthentik/authentik/ci-outpost.yml?branch=main&label=outpost%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/goauthentik/authentik/ci-web.yml?branch=main&label=web%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
[![Code Coverage](https://img.shields.io/codecov/c/gh/goauthentik/authentik?style=for-the-badge)](https://codecov.io/gh/goauthentik/authentik)
![Docker pulls](https://img.shields.io/docker/pulls/authentik/server.svg?style=for-the-badge)
![Latest version](https://img.shields.io/docker/v/authentik/server?sort=semver&style=for-the-badge)
[![](https://img.shields.io/badge/Help%20translate-transifex-blue?style=for-the-badge)](https://www.transifex.com/authentik/authentik/)

View File

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2025.10.0-rc1"
VERSION = "2025.8.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -1,8 +1,5 @@
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
from collections.abc import Callable
from typing import Any
from django.utils.translation import gettext_lazy as _
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.plumbing import (
@@ -11,7 +8,6 @@ from drf_spectacular.plumbing import (
build_basic_type,
build_object_type,
)
from drf_spectacular.renderers import OpenApiJsonRenderer
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import OpenApiTypes
from rest_framework.settings import api_settings
@@ -19,28 +15,34 @@ from rest_framework.settings import api_settings
from authentik.api.apps import AuthentikAPIConfig
from authentik.api.pagination import PAGINATION_COMPONENT_NAME, PAGINATION_SCHEMA
def build_standard_type(obj, **kwargs):
"""Build a basic type with optional add owns."""
schema = build_basic_type(obj)
schema.update(kwargs)
return schema
GENERIC_ERROR = build_object_type(
description=_("Generic API Error"),
properties={
"detail": build_basic_type(OpenApiTypes.STR),
"code": build_basic_type(OpenApiTypes.STR),
"detail": build_standard_type(OpenApiTypes.STR),
"code": build_standard_type(OpenApiTypes.STR),
},
required=["detail"],
)
VALIDATION_ERROR = build_object_type(
description=_("Validation Error"),
properties={
api_settings.NON_FIELD_ERRORS_KEY: build_array_type(build_basic_type(OpenApiTypes.STR)),
"code": build_basic_type(OpenApiTypes.STR),
api_settings.NON_FIELD_ERRORS_KEY: build_array_type(build_standard_type(OpenApiTypes.STR)),
"code": build_standard_type(OpenApiTypes.STR),
},
required=[],
additionalProperties={},
)
def create_component(
generator: SchemaGenerator, name: str, schema: Any, type_=ResolvedComponent.SCHEMA
) -> ResolvedComponent:
def create_component(generator: SchemaGenerator, name, schema, type_=ResolvedComponent.SCHEMA):
"""Register a component and return a reference to it."""
component = ResolvedComponent(
name=name,
@@ -52,18 +54,7 @@ def create_component(
return component
def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
"""Filter out all API Views which are not mounted under /api"""
return [
(path, path_regex, method, callback)
for path, path_regex, method, callback in endpoints
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
]
def postprocess_schema_responses(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
"""Workaround to set a default response for endpoints.
Workaround suggested at
<https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357>
@@ -113,81 +104,10 @@ def postprocess_schema_responses(
return result
def postprocess_schema_pagination(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
declare them globally and refer to them"""
to_replace = {
"ordering": create_component(
generator,
"QueryPaginationOrdering",
{
"name": "ordering",
"required": False,
"in": "query",
"description": "Which field to use when ordering the results.",
"schema": {"type": "string"},
},
ResolvedComponent.PARAMETER,
),
"page": create_component(
generator,
"QueryPaginationPage",
{
"name": "page",
"required": False,
"in": "query",
"description": "A page number within the paginated result set.",
"schema": {"type": "integer"},
},
ResolvedComponent.PARAMETER,
),
"page_size": create_component(
generator,
"QueryPaginationPageSize",
{
"name": "page_size",
"required": False,
"in": "query",
"description": "Number of results to return per page.",
"schema": {"type": "integer"},
},
ResolvedComponent.PARAMETER,
),
"search": create_component(
generator,
"QuerySearch",
{
"name": "search",
"required": False,
"in": "query",
"description": "A search term.",
"schema": {"type": "string"},
},
ResolvedComponent.PARAMETER,
),
}
for path in result["paths"].values():
for method in path.values():
for idx, param in enumerate(method.get("parameters", [])):
for replace_name, replace_ref in to_replace.items():
if param["name"] == replace_name:
method["parameters"][idx] = replace_ref.ref
return result
def postprocess_schema_remove_unused(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Remove unused components"""
# To check if the schema is used, render it to JSON and then substring check that
# less efficient than walking through the tree but a lot simpler and no
# possibility that we miss something
raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
for key in result["components"][ResolvedComponent.SCHEMA].keys():
if raw.count(key) > 1:
continue
del generator.registry._components[(key, ResolvedComponent.SCHEMA)]
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
return result
def preprocess_schema_exclude_non_api(endpoints, **kwargs):
"""Filter out all API Views which are not mounted under /api"""
return [
(path, path_regex, method, callback)
for path, path_regex, method, callback in endpoints
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
]

View File

@@ -76,7 +76,6 @@ 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
from authentik.tenants.models import Tenant
@@ -136,7 +135,6 @@ def excluded_models() -> list[type[Model]]:
EndpointDeviceConnection,
DeviceToken,
StreamEvent,
UserConsent,
)

View File

@@ -113,7 +113,7 @@ class Brand(SerializerModel):
try:
return self.attributes.get("settings", {}).get("locale", "")
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Failed to get default locale", exc=exc)
return ""

View File

@@ -29,8 +29,8 @@ from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
class PartialUserSerializer(ModelSerializer):
"""Partial User Serializer, does not include child relations."""
class GroupMemberSerializer(ModelSerializer):
"""Stripped down user serializer to show relevant users for groups"""
attributes = JSONDictField(required=False)
uid = CharField(read_only=True)
@@ -94,11 +94,11 @@ class GroupSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_children", "false")).lower() == "true"
@extend_schema_field(PartialUserSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[PartialUserSerializer] | None:
@extend_schema_field(GroupMemberSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
if not self._should_include_users:
return None
return PartialUserSerializer(instance.users, many=True).data
return GroupMemberSerializer(instance.users, many=True).data
@extend_schema_field(GroupChildSerializer(many=True))
def get_children_obj(self, instance: Group) -> list[GroupChildSerializer] | None:
@@ -295,7 +295,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
@extend_schema(
request=UserAccountSerializer,
responses={
204: OpenApiResponse(description="User removed"),
204: OpenApiResponse(description="User added"),
404: OpenApiResponse(description="User not found"),
},
)
@@ -307,7 +307,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
permission_classes=[],
)
def remove_user(self, request: Request, pk: str) -> Response:
"""Remove user from group"""
"""Add user to group"""
group: Group = self.get_object()
user: User = (
get_objects_for_user(request.user, "authentik_core.view_user")

View File

@@ -171,7 +171,7 @@ class PropertyMappingViewSet(
except PropertyMappingExpressionException as exc:
response_data["result"] = exception_to_string(exc.exc)
response_data["successful"] = False
except Exception as exc: # noqa
except Exception as exc:
response_data["result"] = exception_to_string(exc)
response_data["successful"] = False
response = PropertyMappingTestResultSerializer(response_data)

View File

@@ -97,8 +97,8 @@ class ParamUserSerializer(PassiveSerializer):
user = PrimaryKeyRelatedField(queryset=User.objects.all().exclude_anonymous(), required=False)
class PartialGroupSerializer(ModelSerializer):
"""Partial Group Serializer, does not include child relations."""
class UserGroupSerializer(ModelSerializer):
"""Simplified Group Serializer for user's groups"""
attributes = JSONDictField(required=False)
parent_name = CharField(source="parent.name", read_only=True, allow_null=True)
@@ -143,11 +143,11 @@ class UserSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_groups", "true")).lower() == "true"
@extend_schema_field(PartialGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[PartialGroupSerializer] | None:
@extend_schema_field(UserGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[UserGroupSerializer] | None:
if not self._should_include_groups:
return None
return PartialGroupSerializer(instance.ak_groups, many=True).data
return UserGroupSerializer(instance.ak_groups, many=True).data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -681,7 +681,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
},
),
responses={
204: OpenApiResponse(description="Successfully started impersonation"),
"204": OpenApiResponse(description="Successfully started impersonation"),
"401": OpenApiResponse(description="Access denied"),
},
)
@action(detail=True, methods=["POST"], permission_classes=[])
@@ -700,7 +701,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"User attempted to impersonate without permissions",
user=request.user,
)
return Response(status=403)
return Response(status=401)
if user_to_be.pk == self.request.user.pk:
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
return Response(status=401)
@@ -709,19 +710,19 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"User attempted to impersonate without providing a reason",
user=request.user,
)
raise ValidationError({"reason": _("This field is required.")})
return Response(status=401)
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED, reason=reason).from_http(request, user_to_be)
return Response(status=204)
return Response(status=201)
@extend_schema(
request=None,
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully ended impersonation"),
"204": OpenApiResponse(description="Successfully started impersonation"),
},
)
@action(detail=False, methods=["GET"])

View File

@@ -21,6 +21,8 @@ from rest_framework.serializers import (
raise_errors_on_nested_writes,
)
from authentik.rbac.permissions import assign_initial_permissions
def is_dict(value: Any):
"""Ensure a value is a dictionary, useful for JSONFields"""
@@ -50,6 +52,15 @@ class ModelSerializer(BaseModelSerializer):
serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy()
serializer_field_mapping[models.JSONField] = JSONDictField
def create(self, validated_data):
instance = super().create(validated_data)
request = self.context.get("request")
if request and hasattr(request, "user") and not request.user.is_anonymous:
assign_initial_permissions(request.user, instance)
return instance
def update(self, instance: Model, validated_data):
raise_errors_on_nested_writes("update", self, validated_data)
info = model_meta.get_field_info(instance)

View File

@@ -1,6 +1,6 @@
"""custom runserver command"""
from io import StringIO
from typing import TextIO
from daphne.management.commands.runserver import Command as RunServer
from daphne.server import Server
@@ -33,4 +33,4 @@ class Command(RunServer):
super().__init__(*args, **kwargs)
# Redirect standard stdout banner from Daphne into the void
# as there are a couple more steps that happen before startup is fully done
self.stdout = StringIO()
self.stdout = TextIO()

View File

@@ -99,7 +99,7 @@ class Command(BaseCommand):
else:
try:
hook()
except Exception: # noqa
except Exception:
# Match the behavior of the cpython shell where an error in
# sys.__interactivehook__ prints a warning and the exception
# and continues.

View File

@@ -114,21 +114,15 @@ class AttributesMixin(models.Model):
def update_attributes(self, properties: dict[str, Any]):
"""Update fields and attributes, but correctly by merging dicts"""
needs_update = False
for key, value in properties.items():
if key == "attributes":
continue
if getattr(self, key, None) != value:
setattr(self, key, value)
needs_update = True
setattr(self, key, value)
final_attributes = {}
MERGE_LIST_UNIQUE.merge(final_attributes, self.attributes)
MERGE_LIST_UNIQUE.merge(final_attributes, properties.get("attributes", {}))
if self.attributes != final_attributes:
self.attributes = final_attributes
needs_update = True
if needs_update:
self.save()
self.attributes = final_attributes
self.save()
@classmethod
def update_or_create_attributes(
@@ -409,7 +403,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
try:
return self.attributes.get("settings", {}).get("locale", "")
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Failed to get default locale", exc=exc)
if request:
return request.brand.locale
@@ -590,7 +584,7 @@ class Application(SerializerModel, PolicyBindingModel):
try:
return url % user.__dict__
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Failed to format launch url", exc=exc)
return url
return url
@@ -786,7 +780,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"slug": self.slug,
}
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Failed to template user path", exc=exc, source=self)
return User.default_path()

View File

@@ -2,9 +2,10 @@
from django.contrib.auth.signals import user_logged_in
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import Signal, receiver
from django.dispatch import receiver
from django.http.request import HttpRequest
from structlog.stdlib import get_logger

View File

@@ -14,7 +14,6 @@ from authentik.core.models import (
ExpiringModel,
User,
)
from authentik.lib.utils.db import chunked_queryset
from authentik.tasks.models import Task
LOGGER = get_logger()
@@ -29,7 +28,7 @@ def clean_expired_models():
cls.objects.all().exclude(expiring=False).exclude(expiring=True, expires__gt=now())
)
amount = objects.count()
for obj in chunked_queryset(objects):
for obj in objects:
obj.expire_action()
LOGGER.debug("Expired models", model=cls, amount=amount)
self.info(f"Expired {amount} {cls._meta.verbose_name_plural}")

View File

@@ -8,7 +8,6 @@
{% endblock %}
{% block body %}
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container alignment="bottom"></ak-message-container>
<ak-interface-admin>
<ak-loading></ak-loading>

View File

@@ -8,7 +8,6 @@
{% endblock %}
{% block body %}
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container></ak-message-container>
<ak-interface-user>
<ak-loading></ak-loading>

View File

@@ -45,7 +45,6 @@
{% block body %}
<div class="pf-c-background-image">
</div>
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container></ak-message-container>
<div class="pf-c-login stacked">
<div class="ak-login-container">

View File

@@ -59,7 +59,7 @@ class TestImpersonation(APITestCase):
),
data={"reason": "some reason"},
)
self.assertEqual(response.status_code, 204)
self.assertEqual(response.status_code, 201)
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
@@ -80,7 +80,7 @@ class TestImpersonation(APITestCase):
),
data={"reason": "some reason"},
)
self.assertEqual(response.status_code, 204)
self.assertEqual(response.status_code, 201)
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
@@ -137,10 +137,10 @@ class TestImpersonation(APITestCase):
self.client.force_login(self.user)
response = self.client.post(
reverse("authentik_api:user-impersonate", kwargs={"pk": self.other_user.pk}),
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}),
data={"reason": ""},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.status_code, 401)
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())

View File

@@ -20,11 +20,6 @@ from authentik.lib.models import CreatedUpdatedModel, SerializerModel
LOGGER = get_logger()
def fingerprint_sha256(cert: Certificate) -> str:
"""Get SHA256 Fingerprint of certificate"""
return hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8")
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."""
@@ -87,7 +82,7 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
@property
def fingerprint_sha256(self) -> str:
"""Get SHA256 Fingerprint of certificate_data"""
return fingerprint_sha256(self.certificate)
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode("utf-8")
@property
def fingerprint_sha1(self) -> str:

View File

@@ -27,7 +27,7 @@ class TestCrypto(APITestCase):
def test_model_private(self):
"""Test model private key"""
cert = CertificateKeyPair.objects.create(
name=generate_id(),
name="test",
certificate_data="foo",
key_data="foo",
)
@@ -271,7 +271,7 @@ class TestCrypto(APITestCase):
keypair = create_test_cert()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
@@ -303,7 +303,7 @@ class TestCrypto(APITestCase):
keypair = create_test_cert()
OAuth2Provider.objects.create(
name=generate_id(),
client_id=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],

View File

@@ -4,7 +4,7 @@ from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import PartialGroupSerializer
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
@@ -13,7 +13,7 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
"""GoogleWorkspaceProviderGroup Serializer"""
group_obj = PartialGroupSerializer(source="group", read_only=True)
group_obj = UserGroupSerializer(source="group", read_only=True)
class Meta:

View File

@@ -3,7 +3,7 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser
@@ -13,7 +13,7 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
"""GoogleWorkspaceProviderUser Serializer"""
user_obj = PartialUserSerializer(source="user", read_only=True)
user_obj = GroupMemberSerializer(source="user", read_only=True)
class Meta:

View File

@@ -4,7 +4,7 @@ from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import PartialGroupSerializer
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
@@ -13,7 +13,7 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
"""MicrosoftEntraProviderGroup Serializer"""
group_obj = PartialGroupSerializer(source="group", read_only=True)
group_obj = UserGroupSerializer(source="group", read_only=True)
class Meta:

View File

@@ -3,7 +3,7 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser
@@ -13,7 +13,7 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class MicrosoftEntraProviderUserSerializer(ModelSerializer):
"""MicrosoftEntraProviderUser Serializer"""
user_obj = PartialUserSerializer(source="user", read_only=True)
user_obj = GroupMemberSerializer(source="user", read_only=True)
class Meta:

View File

@@ -1,14 +0,0 @@
from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError
from authentik.crypto.models import CertificateKeyPair
from authentik.enterprise.license import LicenseKey
class RadiusProviderSerializerMixin:
def validate_certificate(self, cert: CertificateKeyPair) -> CertificateKeyPair:
if cert:
if not LicenseKey.cached_summary().status.is_valid:
raise ValidationError(_("Enterprise is required to use EAP-TLS."))
return cert

View File

@@ -1,9 +0,0 @@
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEnterpriseProviderRadiusConfig(EnterpriseConfig):
name = "authentik.enterprise.providers.radius"
label = "authentik_enterprise_providers_radius"
verbose_name = "authentik Enterprise.Providers.Radius"
default = True

View File

@@ -1,14 +0,0 @@
from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError
from authentik.enterprise.license import LicenseKey
from authentik.providers.scim.models import SCIMAuthenticationMode
class SCIMProviderSerializerMixin:
def validate_auth_mode(self, auth_mode: SCIMAuthenticationMode) -> SCIMAuthenticationMode:
if auth_mode == SCIMAuthenticationMode.OAUTH:
if not LicenseKey.cached_summary().status.is_valid:
raise ValidationError(_("Enterprise is required to use the OAuth mode."))
return auth_mode

View File

@@ -1,9 +0,0 @@
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEnterpriseProviderSCIMConfig(EnterpriseConfig):
name = "authentik.enterprise.providers.scim"
label = "authentik_enterprise_providers_scim"
verbose_name = "authentik Enterprise.Providers.SCIM"
default = True

View File

@@ -1,80 +0,0 @@
from datetime import timedelta
from typing import TYPE_CHECKING
from django.utils.timezone import now
from requests import Request, RequestException
from structlog.stdlib import get_logger
from authentik.providers.scim.clients.exceptions import SCIMRequestException
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
if TYPE_CHECKING:
from authentik.providers.scim.models import SCIMProvider
class SCIMOAuthException(SCIMRequestException):
"""Exceptions related to OAuth operations for SCIM requests"""
class SCIMOAuthAuth:
def __init__(self, provider: "SCIMProvider"):
self.provider = provider
self.user = provider.auth_oauth_user
self.connection = self.get_connection()
self.logger = get_logger().bind()
def retrieve_token(self):
if not self.provider.auth_oauth:
return None
source: OAuthSource = self.provider.auth_oauth
client = OAuth2Client(source, None)
access_token_url = source.source_type.access_token_url or ""
if source.source_type.urls_customizable and source.access_token_url:
access_token_url = source.access_token_url
data = client.get_access_token_args(None, None)
data["grant_type"] = "password"
data.update(self.provider.auth_oauth_params)
try:
response = client.do_request(
"POST",
access_token_url,
auth=client.get_access_token_auth(),
data=data,
headers=client._default_headers,
)
response.raise_for_status()
body = response.json()
if "error" in body:
self.logger.info("Failed to get new OAuth token", error=body["error"])
raise SCIMOAuthException(response, body["error"])
return body
except RequestException as exc:
raise SCIMOAuthException(exc.response, message="Failed to get OAuth token") from exc
def get_connection(self):
token = UserOAuthSourceConnection.objects.filter(
source=self.provider.auth_oauth, user=self.user, expires__gt=now()
).first()
if token and token.access_token:
return token
token = self.retrieve_token()
access_token = token["access_token"]
expires_in = int(token.get("expires_in", 0))
token, _ = UserOAuthSourceConnection.objects.update_or_create(
source=self.provider.auth_oauth,
user=self.user,
defaults={
"access_token": access_token,
"expires": now() + timedelta(seconds=expires_in),
},
)
return token
def __call__(self, request: Request) -> Request:
if not self.connection.is_valid:
self.logger.info("OAuth token expired, renewing token")
self.connection = self.get_connection()
request.headers["Authorization"] = f"Bearer {self.connection.access_token}"
return request

View File

@@ -1,30 +0,0 @@
from django.db.models import Model
from django.db.models.signals import post_save
from django.dispatch import receiver
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, User, UserTypes
from authentik.events.middleware import audit_ignore
from authentik.providers.scim.models import SCIMAuthenticationMode, SCIMProvider
USER_PATH_PROVIDERS_SCIM = USER_PATH_SYSTEM_PREFIX + "/providers/scim"
@receiver(post_save, sender=SCIMProvider)
def scim_provider_post_save(sender: type[Model], instance: SCIMProvider, created: bool, **__):
"""Create service account before provider is saved"""
identifier = f"ak-providers-scim-{instance.pk}"
with audit_ignore():
if instance.auth_mode == SCIMAuthenticationMode.OAUTH:
user, user_created = User.objects.update_or_create(
username=identifier,
defaults={
"name": f"SCIM Provider {instance.name} Service-Account",
"type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
"path": USER_PATH_PROVIDERS_SCIM,
},
)
if created or user_created:
instance.auth_oauth_user = user
instance.save()
elif instance.auth_mode == SCIMAuthenticationMode.TOKEN:
User.objects.filter(username=identifier).delete()

View File

@@ -1,193 +0,0 @@
"""SCIM OAuth tests"""
from base64 import b64encode
from datetime import timedelta
from unittest.mock import MagicMock, PropertyMock, patch
from django.urls import reverse
from django.utils.timezone import now
from requests_mock import Mocker
from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import License
from authentik.enterprise.tests.test_license import expiry_valid
from authentik.lib.generators import generate_id
from authentik.providers.scim.models import SCIMAuthenticationMode, SCIMMapping, SCIMProvider
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.tenants.models import Tenant
class SCIMOAuthTests(APITestCase):
"""SCIM User tests"""
@apply_blueprint("system/providers-scim.yaml")
def setUp(self) -> None:
# Delete all users and groups as the mocked HTTP responses only return one ID
# which will cause errors with multiple users
Tenant.objects.update(avatars="none")
User.objects.all().exclude_anonymous().delete()
Group.objects.all().delete()
self.source = OAuthSource.objects.create(
name=generate_id(),
slug=generate_id(),
access_token_url="http://localhost/token", # nosec
consumer_key=generate_id(),
consumer_secret=generate_id(),
provider_type="openidconnect",
)
self.provider = SCIMProvider.objects.create(
name=generate_id(),
url="https://localhost",
auth_mode=SCIMAuthenticationMode.OAUTH,
auth_oauth=self.source,
auth_oauth_params={
"foo": "bar",
},
exclude_users_service_account=True,
)
self.app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
)
self.provider.property_mappings_group.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
)
def test_retrieve_token(self):
"""Test token retrieval"""
with Mocker() as mocker:
token = generate_id()
mocker.post("http://localhost/token", json={"access_token": token, "expires_in": 3600})
self.provider.scim_auth()
conn = UserOAuthSourceConnection.objects.filter(
source=self.source,
user=self.provider.auth_oauth_user,
).first()
self.assertIsNotNone(conn)
self.assertTrue(conn.is_valid)
auth = (
b64encode(
b":".join((self.source.consumer_key.encode(), self.source.consumer_secret.encode()))
)
.strip()
.decode()
)
self.assertEqual(
mocker.request_history[0].headers["Authorization"],
f"Basic {auth}",
)
self.assertEqual(mocker.request_history[0].body, "grant_type=password&foo=bar")
def test_existing_token(self):
"""Test existing token"""
UserOAuthSourceConnection.objects.create(
source=self.source,
user=self.provider.auth_oauth_user,
access_token=generate_id(),
expires=now() + timedelta(hours=3),
)
with Mocker() as mocker:
self.provider.scim_auth()
self.assertEqual(len(mocker.request_history), 0)
@Mocker()
def test_user_create(self, mock: Mocker):
"""Test user creation"""
scim_id = generate_id()
token = generate_id()
mock.post("http://localhost/token", json={"access_token": token, "expires_in": 3600})
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
)
mock.post(
"https://localhost/Users",
json={
"id": scim_id,
},
)
uid = generate_id()
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 3)
self.assertEqual(mock.request_history[1].method, "GET")
self.assertEqual(mock.request_history[2].method, "POST")
self.assertJSONEqual(
mock.request_history[2].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
"primary": True,
"type": "other",
"value": f"{uid}@goauthentik.io",
}
],
"externalId": user.uid,
"name": {
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": f"{uid} {uid}",
"userName": uid,
},
)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=expiry_valid,
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_api_create(self):
License.objects.create(key=generate_id())
self.client.force_login(create_test_admin_user())
res = self.client.post(
reverse("authentik_api:scimprovider-list"),
{
"name": generate_id(),
"url": "http://localhost",
"auth_mode": "oauth",
"auth_oauth": str(self.source.pk),
},
)
self.assertEqual(res.status_code, 201)
@patch(
"authentik.enterprise.models.LicenseUsageStatus.is_valid",
PropertyMock(return_value=False),
)
def test_api_create_no_license(self):
self.client.force_login(create_test_admin_user())
res = self.client.post(
reverse("authentik_api:scimprovider-list"),
{
"name": generate_id(),
"url": "http://localhost",
"auth_mode": "oauth",
"auth_oauth": str(self.source.pk),
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(
res.content, {"auth_mode": ["Enterprise is required to use the OAuth mode."]}
)

View File

@@ -1,8 +1,6 @@
SPECTACULAR_SETTINGS = {
"POSTPROCESSING_HOOKS": [
"authentik.api.schema.postprocess_schema_responses",
"authentik.api.schema.postprocess_schema_pagination",
"authentik.api.schema.postprocess_schema_remove_unused",
"authentik.enterprise.search.schema.postprocess_schema_search_autocomplete",
"drf_spectacular.hooks.postprocess_schema_enums",
],

View File

@@ -5,8 +5,6 @@ TENANT_APPS = [
"authentik.enterprise.policies.unique_password",
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.radius",
"authentik.enterprise.providers.scim",
"authentik.enterprise.providers.ssf",
"authentik.enterprise.search",
"authentik.enterprise.stages.authenticator_endpoint_gdtc",

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.12 on 2025-09-08 19:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_authenticator_endpoint_gdtc", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="authenticatorendpointgdtcstage",
name="friendly_name",
field=models.TextField(blank=True, default=""),
preserve_default=False,
),
]

View File

@@ -7,8 +7,6 @@ from cryptography.x509 import (
Certificate,
NameOID,
ObjectIdentifier,
RFC822Name,
SubjectAlternativeName,
UnsupportedGeneralNameType,
load_pem_x509_certificate,
)
@@ -17,7 +15,7 @@ from django.utils.translation import gettext_lazy as _
from authentik.brands.models import Brand
from authentik.core.models import User
from authentik.crypto.models import CertificateKeyPair, fingerprint_sha256
from authentik.crypto.models import CertificateKeyPair
from authentik.enterprise.stages.mtls.models import (
CertAttributes,
MutualTLSStage,
@@ -139,7 +137,7 @@ class MTLSStageView(ChallengeStageView):
case CertAttributes.COMMON_NAME:
cert_attr = self.get_cert_attribute(cert, NameOID.COMMON_NAME)
case CertAttributes.EMAIL:
cert_attr = self.get_cert_email(cert)
cert_attr = self.get_cert_attribute(cert, NameOID.EMAIL_ADDRESS)
match stage.user_attribute:
case UserAttributes.USERNAME:
user_attr = "username"
@@ -173,7 +171,7 @@ class MTLSStageView(ChallengeStageView):
self.executor.plan.context.setdefault(PLAN_CONTEXT_PROMPT, {})
self.executor.plan.context[PLAN_CONTEXT_PROMPT].update(
{
"email": self.get_cert_email(cert),
"email": self.get_cert_attribute(cert, NameOID.EMAIL_ADDRESS),
"name": self.get_cert_attribute(cert, NameOID.COMMON_NAME),
}
)
@@ -185,13 +183,6 @@ class MTLSStageView(ChallengeStageView):
return None
return str(attr[0].value)
def get_cert_email(self, cert: Certificate) -> str | None:
ext = cert.extensions.get_extension_for_class(SubjectAlternativeName)
_cert_attr = ext.value.get_values_for_type(RFC822Name)
if len(_cert_attr) < 1:
return None
return str(_cert_attr[0])
def dispatch(self, request, *args, **kwargs):
stage: MutualTLSStage = self.executor.current_stage
certs = [
@@ -219,7 +210,6 @@ class MTLSStageView(ChallengeStageView):
if not cert and stage.mode == TLSMode.OPTIONAL:
self.logger.info("No certificate given, continuing")
return self.executor.stage_ok()
self.logger.debug("Received certificate", cert=fingerprint_sha256(cert))
existing_user = self.check_if_user(cert)
if self.executor.flow.designation == FlowDesignation.ENROLLMENT:
self.enroll_prepare_user(cert)

View File

@@ -19,7 +19,7 @@ if TYPE_CHECKING:
class ASNDict(TypedDict):
"""ASN Details"""
asn: int | None
asn: int
as_org: str | None
network: str | None
@@ -60,7 +60,7 @@ class ASNContextProcessor(MMDBContextProcessor):
except (GeoIP2Error, ValueError):
return None
def asn_to_dict(self, asn: ASN | None) -> ASNDict | dict:
def asn_to_dict(self, asn: ASN | None) -> ASNDict:
"""Convert ASN to dict"""
if not asn:
return {}

View File

@@ -19,10 +19,10 @@ if TYPE_CHECKING:
class GeoIPDict(TypedDict):
"""GeoIP Details"""
continent: str | None
country: str | None
lat: float | None
long: float | None
continent: str
country: str
lat: float
long: float
city: str
@@ -61,7 +61,7 @@ class GeoIPContextProcessor(MMDBContextProcessor):
except (GeoIP2Error, ValueError):
return None
def city_to_dict(self, city: City | None) -> GeoIPDict | dict:
def city_to_dict(self, city: City | None) -> GeoIPDict:
"""Convert City to dict"""
if not city:
return {}

View File

@@ -197,8 +197,7 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
current_request = _CTX_REQUEST.get()
if current_request is None or request.request_id != current_request.request_id:
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)
@@ -213,8 +212,7 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
current_request = _CTX_REQUEST.get()
if current_request is None or request.request_id != current_request.request_id:
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)
@@ -241,8 +239,7 @@ class AuditMiddleware:
return
if _CTX_IGNORE.get():
return
current_request = _CTX_REQUEST.get()
if current_request is None or request.request_id != current_request.request_id:
if request.request_id != _CTX_REQUEST.get().request_id:
return
user = self.get_user(request)

View File

@@ -1,16 +0,0 @@
# Generated by Django 5.1.11 on 2025-07-28 15:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0012_notificationtransport_email_subject_prefix_and_more"),
]
operations = [
migrations.DeleteModel(
name="SystemTask",
),
]

View File

@@ -632,3 +632,45 @@ class NotificationWebhookMapping(PropertyMapping):
class Meta:
verbose_name = _("Webhook Mapping")
verbose_name_plural = _("Webhook Mappings")
class TaskStatus(models.TextChoices):
"""DEPRECATED do not use"""
UNKNOWN = "unknown"
SUCCESSFUL = "successful"
WARNING = "warning"
ERROR = "error"
class SystemTask(ExpiringModel):
"""DEPRECATED do not use"""
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
name = models.TextField()
uid = models.TextField(null=True)
start_timestamp = models.DateTimeField(default=now)
finish_timestamp = models.DateTimeField(default=now)
duration = models.FloatField(default=0)
status = models.TextField(choices=TaskStatus.choices)
description = models.TextField(null=True)
messages = models.JSONField()
task_call_module = models.TextField()
task_call_func = models.TextField()
task_call_args = models.JSONField(default=list)
task_call_kwargs = models.JSONField(default=dict)
def __str__(self) -> str:
return f"System Task {self.name}"
class Meta:
unique_together = (("name", "uid"),)
default_permissions = ()
permissions = ()
verbose_name = _("System Task")
verbose_name_plural = _("System Tasks")
indexes = ExpiringModel.Meta.indexes

View File

@@ -16,7 +16,6 @@ from authentik.events.models import (
NotificationRule,
NotificationTransport,
)
from authentik.lib.utils.db import chunked_queryset
from authentik.policies.engine import PolicyEngine
from authentik.policies.models import PolicyBinding, PolicyEngineMode
from authentik.tasks.models import Task
@@ -124,8 +123,7 @@ def gdpr_cleanup(user_pk: int):
"""cleanup events from gdpr_compliance"""
events = Event.objects.filter(user__pk=user_pk)
LOGGER.debug("GDPR cleanup, removing events from user", events=events.count())
for event in chunked_queryset(events):
event.delete()
events.delete()
@actor(description=_("Cleanup seen notifications and notifications whose event expired."))

View File

@@ -291,7 +291,7 @@ class ConfigurableStage(models.Model):
class FriendlyNamedStage(models.Model):
"""Abstract base class for a Stage that can have a user friendly name configured."""
friendly_name = models.TextField(blank=True)
friendly_name = models.TextField(null=True)
class Meta:
abstract = True

View File

@@ -160,7 +160,7 @@ class ChallengeStageView(StageView):
"user": self.get_pending_user(for_display=True),
}
except Exception as exc: # noqa
except Exception as exc:
self.logger.warning("failed to template title", exc=exc)
return self.executor.flow.title
@@ -286,12 +286,6 @@ class SessionEndStage(ChallengeStageView):
that the user is likely to take after signing out of a provider."""
def get_challenge(self, *args, **kwargs) -> Challenge:
if not self.request.user.is_authenticated:
return RedirectChallenge(
data={
"to": reverse("authentik_core:root-redirect"),
},
)
application: Application | None = self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION)
data = {
"component": "ak-stage-session-end",

View File

@@ -27,7 +27,6 @@ window.authentik.flow = {
{% endblock %}
{% block body %}
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container></ak-message-container>
<ak-flow-executor flowSlug="{{ flow.slug }}">
<ak-loading></ak-loading>

View File

@@ -198,7 +198,7 @@ class FlowExecutorView(APIView):
# if the cached plan is from an older version, it might have different attributes
# in which case we just delete the plan and invalidate everything
next_binding = self.plan.next(self.request)
except Exception as exc: # noqa
except Exception as exc:
self._logger.warning(
"f(exec): found incompatible flow plan, invalidating run", exc=exc
)
@@ -288,7 +288,7 @@ class FlowExecutorView(APIView):
span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.dispatch(request)
return to_stage_response(request, stage_response)
except Exception as exc: # noqa
except Exception as exc:
return self.handle_exception(exc)
@extend_schema(
@@ -339,7 +339,7 @@ class FlowExecutorView(APIView):
span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.dispatch(request)
return to_stage_response(request, stage_response)
except Exception as exc: # noqa
except Exception as exc:
return self.handle_exception(exc)
def _initiate_plan(self) -> FlowPlan:
@@ -351,7 +351,7 @@ class FlowExecutorView(APIView):
# there are no issues with the class we might've gotten
# from the cache. If there are errors, just delete all cached flows
_ = plan.has_stages
except Exception: # noqa
except Exception:
keys = cache.keys(f"{CACHE_PREFIX}*")
cache.delete_many(keys)
return self._initiate_plan()

View File

@@ -1,29 +0,0 @@
"""authentik database utilities"""
import gc
from django.db import reset_queries
from django.db.models import QuerySet
def chunked_queryset(queryset: QuerySet, chunk_size: int = 1_000):
if not queryset.exists():
return []
def get_chunks(qs: QuerySet):
qs = qs.order_by("pk")
pks = qs.values_list("pk", flat=True)
start_pk = pks[0]
while True:
try:
end_pk = pks.filter(pk__gte=start_pk)[chunk_size]
except IndexError:
break
yield qs.filter(pk__gte=start_pk, pk__lt=end_pk)
start_pk = end_pk
yield qs.filter(pk__gte=start_pk)
for chunk in get_chunks(queryset):
reset_queries()
gc.collect()
yield from chunk.iterator()

View File

@@ -6,7 +6,6 @@ from pathlib import Path
from tempfile import gettempdir
from django.conf import settings
from django.utils.module_loading import import_string
from authentik.lib.config import CONFIG
@@ -63,13 +62,3 @@ def get_env() -> str:
if "AK_APPLIANCE" in os.environ:
return os.environ["AK_APPLIANCE"]
return "custom"
def ConditionalInheritance(path: str):
"""Conditionally inherit from a class, intended for things like authentik.enterprise,
without which authentik should still be able to run"""
try:
cls = import_string(path)
return cls
except ModuleNotFoundError:
return object

View File

@@ -357,7 +357,7 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
message=(
"While setting the permissions for the service-account, a "
"permission was not found: Check "
"https://docs.goauthentik.io/troubleshooting/missing_permission"
"https://goauthentik.io/docs/troubleshooting/missing_permission"
),
).with_exception(exc).set_user(user).save()
else:

View File

@@ -10,9 +10,9 @@ from rest_framework.serializers import PrimaryKeyRelatedField
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import PartialGroupSerializer
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.models import PolicyBinding, PolicyBindingModel
@@ -61,8 +61,8 @@ class PolicyBindingSerializer(ModelSerializer):
)
policy_obj = PolicySerializer(required=False, read_only=True, source="policy")
group_obj = PartialGroupSerializer(required=False, read_only=True, source="group")
user_obj = PartialUserSerializer(required=False, read_only=True, source="user")
group_obj = GroupSerializer(required=False, read_only=True, source="group")
user_obj = UserSerializer(required=False, read_only=True, source="user")
class Meta:
model = PolicyBinding

View File

@@ -71,7 +71,7 @@ class PolicyEvaluator(BaseEvaluator):
# PolicyExceptions should be propagated back to the process,
# which handles recording and returning a correct result
raise exc
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Expression error", exc=exc)
return PolicyResult(False, str(exc))
else:

View File

@@ -144,6 +144,6 @@ class PolicyProcess(PROCESS_CLASS):
"""Task wrapper to run policy checking"""
try:
self.connection.send(self.profiling_wrapper())
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Policy failed to run", exc=exc)
self.connection.send(PolicyResult(False, str(exc)))

View File

@@ -66,7 +66,6 @@ class OAuth2ProviderSerializer(ProviderSerializer):
"access_code_validity",
"access_token_validity",
"refresh_token_validity",
"refresh_token_threshold",
"include_claims_in_id_token",
"signing_key",
"encryption_key",

View File

@@ -147,12 +147,11 @@ class IDToken:
id_dict.update(self.claims)
return id_dict
def to_access_token(self, provider: "OAuth2Provider", token: "BaseGrantModel") -> str:
def to_access_token(self, provider: "OAuth2Provider") -> str:
"""Encode id_token for use as access token, adding fields"""
final = self.to_dict()
final["azp"] = provider.client_id
final["uid"] = generate_id()
final["scope"] = " ".join(token.scope)
return provider.encode(final)
def to_jwt(self, provider: "OAuth2Provider") -> str:

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.1.12 on 2025-09-25 15:26
import authentik.lib.utils.time
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0029_oauth2provider__backchannel_logout_uris"),
]
operations = [
migrations.AddField(
model_name="oauth2provider",
name="refresh_token_threshold",
field=models.TextField(
default="seconds=0",
help_text="When refreshing a token, if the refresh token is valid for less than this duration, it will be renewed. When set to seconds=0, token will always be renewed. (Format: hours=1;minutes=2;seconds=3).",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
]

View File

@@ -238,16 +238,6 @@ class OAuth2Provider(WebfingerProvider, Provider):
"(Format: hours=1;minutes=2;seconds=3)."
),
)
refresh_token_threshold = models.TextField(
default="seconds=0",
validators=[timedelta_string_validator],
help_text=_(
"When refreshing a token, if the refresh token is valid for less than "
"this duration, it will be renewed. "
"When set to seconds=0, token will always be renewed. "
"(Format: hours=1;minutes=2;seconds=3)."
),
)
sub_mode = models.TextField(
choices=SubModes.choices,
@@ -507,7 +497,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
@id_token.setter
def id_token(self, value: "IDToken"):
self.token = value.to_access_token(self.provider, self)
self.token = value.to_access_token(self.provider)
self._id_token = json.dumps(asdict(value))
@property

View File

@@ -376,63 +376,3 @@ class TestToken(OAuthTestCase):
)
self.assertEqual(response.status_code, 400)
self.assertTrue(Event.objects.filter(action=EventAction.SUSPICIOUS_REQUEST).exists())
@apply_blueprint("system/providers-oauth2.yaml")
def test_refresh_token_view_threshold(self):
"""test request param"""
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=self.keypair,
refresh_token_threshold="hours=1", # nosec
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
managed__in=[
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-profile",
"goauthentik.io/providers/oauth2/scope-offline_access",
]
)
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
self.app.save()
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
user = create_test_admin_user()
token: RefreshToken = RefreshToken.objects.create(
provider=provider,
user=user,
token=generate_id(),
_id_token=dumps({}),
auth_time=timezone.now(),
_scope="offline_access",
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
"refresh_token": token.token,
"redirect_uri": "http://local.invalid",
},
HTTP_AUTHORIZATION=f"Basic {header}",
HTTP_ORIGIN="http://local.invalid",
)
self.assertEqual(response["Access-Control-Allow-Credentials"], "true")
self.assertEqual(response["Access-Control-Allow-Origin"], "http://local.invalid")
access: AccessToken = AccessToken.objects.filter(user=user, provider=provider).first()
self.assertJSONEqual(
response.content.decode(),
{
"access_token": access.token,
"token_type": TOKEN_TYPE,
"expires_in": 3600,
"id_token": provider.encode(
access.id_token.to_dict(),
),
"scope": "offline_access",
},
)
self.validate_jwt(access, provider)

View File

@@ -684,8 +684,32 @@ class TokenView(View):
)
access_token.save()
res = {
refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity)
refresh_token = RefreshToken(
user=self.params.refresh_token.user,
scope=self.params.refresh_token.scope,
expires=refresh_token_expiry,
provider=self.provider,
auth_time=self.params.refresh_token.auth_time,
session=self.params.refresh_token.session,
)
id_token = IDToken.new(
self.provider,
refresh_token,
self.request,
)
id_token.nonce = self.params.refresh_token.id_token.nonce
id_token.at_hash = access_token.at_hash
refresh_token.id_token = id_token
refresh_token.save()
# Mark old token as revoked
self.params.refresh_token.revoked = True
self.params.refresh_token.save()
return {
"access_token": access_token.token,
"refresh_token": refresh_token.token,
"token_type": TOKEN_TYPE,
"scope": " ".join(access_token.scope),
"expires_in": int(
@@ -694,37 +718,6 @@ class TokenView(View):
"id_token": access_token.id_token.to_jwt(self.provider),
}
refresh_token_threshold = timedelta_from_string(self.provider.refresh_token_threshold)
if (
refresh_token_threshold.total_seconds() == 0
or (now - self.params.refresh_token.expires) > refresh_token_threshold
):
refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity)
refresh_token = RefreshToken(
user=self.params.refresh_token.user,
scope=self.params.refresh_token.scope,
expires=refresh_token_expiry,
provider=self.provider,
auth_time=self.params.refresh_token.auth_time,
session=self.params.refresh_token.session,
)
id_token = IDToken.new(
self.provider,
refresh_token,
self.request,
)
id_token.nonce = self.params.refresh_token.id_token.nonce
id_token.at_hash = access_token.at_hash
refresh_token.id_token = id_token
refresh_token.save()
# Mark old token as revoked
self.params.refresh_token.revoked = True
self.params.refresh_token.save()
res["refresh_token"] = refresh_token.token
return res
def create_client_credentials_response(self) -> dict[str, Any]:
"""See https://datatracker.ietf.org/doc/html/rfc6749#section-4.4"""
now = timezone.now()

View File

@@ -60,7 +60,7 @@ class UserInfoView(View):
for scope in scopes:
if scope in special_scope_map:
scope_descriptions.append(
PermissionDict(id=str(scope), name=str(special_scope_map[scope]))
PermissionDict(id=scope, name=str(special_scope_map[scope]))
)
return scope_descriptions

View File

@@ -3,7 +3,7 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.providers.rac.api.endpoints import EndpointSerializer
@@ -16,7 +16,7 @@ class ConnectionTokenSerializer(ModelSerializer):
provider_obj = RACProviderSerializer(source="provider", read_only=True)
endpoint_obj = EndpointSerializer(source="endpoint", read_only=True)
user = PartialUserSerializer(source="session.user", read_only=True)
user = GroupMemberSerializer(source="session.user", read_only=True)
class Meta:
model = ConnectionToken

View File

@@ -23,19 +23,13 @@ from authentik.core.models import Application
from authentik.events.models import Event, EventAction
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.utils.reflection import ConditionalInheritance
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
from authentik.providers.radius.models import RadiusProvider, RadiusProviderPropertyMapping
class RadiusProviderSerializer(
ConditionalInheritance(
"authentik.enterprise.providers.radius.api.RadiusProviderSerializerMixin"
),
ProviderSerializer,
):
class RadiusProviderSerializer(ProviderSerializer):
"""RadiusProvider Serializer"""
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
@@ -49,7 +43,6 @@ class RadiusProviderSerializer(
"shared_secret",
"outpost_set",
"mfa_support",
"certificate",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
@@ -85,7 +78,6 @@ class RadiusOutpostConfigSerializer(ModelSerializer):
"client_networks",
"shared_secret",
"mfa_support",
"certificate",
]

View File

@@ -1,25 +0,0 @@
# Generated by Django 5.1.11 on 2025-07-20 17:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0004_alter_certificatekeypair_name"),
("authentik_providers_radius", "0004_alter_radiusproviderpropertymapping_options"),
]
operations = [
migrations.AddField(
model_name="radiusprovider",
name="certificate",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="authentik_crypto.certificatekeypair",
),
),
]

View File

@@ -1,14 +1,11 @@
"""Radius Provider"""
from collections.abc import Iterable
from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import PropertyMapping, Provider
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_id
from authentik.outposts.models import OutpostModel
@@ -41,10 +38,6 @@ class RadiusProvider(OutpostModel, Provider):
),
)
certificate = models.ForeignKey(
CertificateKeyPair, on_delete=models.CASCADE, default=None, null=True
)
@property
def launch_url(self) -> str | None:
"""Radius never has a launch URL"""
@@ -64,12 +57,6 @@ class RadiusProvider(OutpostModel, Provider):
return RadiusProviderSerializer
def get_required_objects(self) -> Iterable[models.Model | str]:
required = [self, "authentik_stages_mtls.pass_outpost_certificate"]
if self.certificate is not None:
required.append(self.certificate)
return required
def __str__(self):
return f"Radius Provider {self.name}"

View File

@@ -239,33 +239,32 @@ class AssertionProcessor:
).from_http(self.http_request)
LOGGER.warning("Failed to evaluate property mapping", exc=exc)
return name_id
if self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_EMAIL:
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL:
name_id.text = self.http_request.user.email
return name_id
if self.auth_n_request.name_id_policy in [
if name_id.attrib["Format"] in [
SAML_NAME_ID_FORMAT_PERSISTENT,
SAML_NAME_ID_FORMAT_UNSPECIFIED,
]:
name_id.text = persistent
return name_id
if self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_X509:
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509:
# This attribute is statically set by the LDAP source
name_id.text = self.http_request.user.attributes.get(
LDAP_DISTINGUISHED_NAME, persistent
)
return name_id
if self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_WINDOWS:
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_WINDOWS:
# This attribute is statically set by the LDAP source
name_id.text = self.http_request.user.attributes.get("upn", persistent)
return name_id
if self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_TRANSIENT:
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
# Use the hash of the user's session, which changes every session
session_key: str = self.http_request.session.session_key
name_id.text = sha256(session_key.encode()).hexdigest()
return name_id
raise UnsupportedNameIDFormat(
"Assertion contains NameID with unsupported "
f"format {self.auth_n_request.name_id_policy}."
f"Assertion contains NameID with unsupported format {name_id.attrib['Format']}."
)
def get_assertion_subject(self) -> Element:

View File

@@ -97,7 +97,6 @@ class TestAuthNRequest(TestCase):
pre_authentication_flow=create_test_flow(),
signing_kp=self.cert,
verification_kp=self.cert,
signed_assertion=True,
)
def test_signed_valid(self):
@@ -172,7 +171,6 @@ class TestAuthNRequest(TestCase):
self.provider.sign_assertion = True
self.provider.sign_response = True
self.provider.save()
self.source.signed_response = True
http_request = get_request("/")
# First create an AuthNRequest

View File

@@ -4,7 +4,7 @@ from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import PartialGroupSerializer
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
from authentik.providers.scim.models import SCIMProviderGroup
@@ -13,7 +13,7 @@ from authentik.providers.scim.models import SCIMProviderGroup
class SCIMProviderGroupSerializer(ModelSerializer):
"""SCIMProviderGroup Serializer"""
group_obj = PartialGroupSerializer(source="group", read_only=True)
group_obj = UserGroupSerializer(source="group", read_only=True)
class Meta:

View File

@@ -5,15 +5,11 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin
from authentik.lib.utils.reflection import ConditionalInheritance
from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_sync, scim_sync_objects
class SCIMProviderSerializer(
ConditionalInheritance("authentik.enterprise.providers.scim.api.SCIMProviderSerializerMixin"),
ProviderSerializer,
):
class SCIMProviderSerializer(ProviderSerializer):
"""SCIMProvider Serializer"""
class Meta:
@@ -32,9 +28,6 @@ class SCIMProviderSerializer(
"url",
"verify_certificates",
"token",
"auth_mode",
"auth_oauth",
"auth_oauth_params",
"compatibility_mode",
"exclude_users_service_account",
"filter_group",

View File

@@ -3,7 +3,7 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
@@ -13,7 +13,7 @@ from authentik.providers.scim.models import SCIMProviderUser
class SCIMProviderUserSerializer(ModelSerializer):
"""SCIMProviderUser Serializer"""
user_obj = PartialUserSerializer(source="user", read_only=True)
user_obj = GroupMemberSerializer(source="user", read_only=True)
class Meta:

View File

@@ -1,16 +0,0 @@
from typing import TYPE_CHECKING
from requests import Request
if TYPE_CHECKING:
from authentik.providers.scim.models import SCIMProvider
class SCIMTokenAuth:
def __init__(self, provider: "SCIMProvider"):
self.provider = provider
def __call__(self, request: Request) -> Request:
request.headers["Authorization"] = f"Bearer {self.provider.token}"
return request

View File

@@ -35,6 +35,7 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
"""SCIM Client"""
base_url: str
token: str
_session: Session
_config: ServiceProviderConfiguration
@@ -44,12 +45,12 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
self._session = get_http_session()
self._session.verify = provider.verify_certificates
self.provider = provider
self.auth = provider.scim_auth()
# Remove trailing slashes as we assume the URL doesn't have any
base_url = provider.url
if base_url.endswith("/"):
base_url = base_url[:-1]
self.base_url = base_url
self.token = provider.token
self._config = self.get_service_provider_config()
def _request(self, method: str, path: str, **kwargs) -> dict:
@@ -61,8 +62,8 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
method,
f"{self.base_url}{path}",
**kwargs,
auth=self.auth,
headers={
"Authorization": f"Bearer {self.token}",
"Accept": "application/scim+json",
"Content-Type": "application/scim+json",
},
@@ -88,11 +89,10 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
def get_service_provider_config(self):
"""Get Service provider config"""
default_config = ServiceProviderConfiguration.default()
path = "/ServiceProviderConfig"
if self.provider.compatibility_mode == SCIMCompatibilityMode.SALESFORCE:
path = "/ServiceProviderConfigs"
try:
config = ServiceProviderConfiguration.model_validate(self._request("GET", path))
config = ServiceProviderConfiguration.model_validate(
self._request("GET", "/ServiceProviderConfig")
)
if self.provider.compatibility_mode == SCIMCompatibilityMode.AWS:
config.patch.supported = False
if self.provider.compatibility_mode == SCIMCompatibilityMode.SLACK:

View File

@@ -1,59 +0,0 @@
# Generated by Django 5.1.12 on 2025-09-23 12:31
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_scim", "0013_scimprovidergroup_attributes_and_more"),
("authentik_sources_oauth", "0011_useroauthsourceconnection_expires"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="scimprovider",
name="auth_mode",
field=models.TextField(
choices=[("token", "Token"), ("oauth", "OAuth")], default="token"
),
),
migrations.AddField(
model_name="scimprovider",
name="auth_oauth",
field=models.ForeignKey(
default=None,
help_text="OAuth Source used for authentication",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_sources_oauth.oauthsource",
),
),
migrations.AddField(
model_name="scimprovider",
name="auth_oauth_params",
field=models.JSONField(
blank=True,
default=dict,
help_text="Additional OAuth parameters, such as grant_type",
),
),
migrations.AddField(
model_name="scimprovider",
name="auth_oauth_user",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="scimprovider",
name="token",
field=models.TextField(blank=True, help_text="Authentication token"),
),
]

View File

@@ -1,32 +0,0 @@
# Generated by Django 5.1.12 on 2025-09-24 12:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_scim",
"0014_scimprovider_auth_mode_scimprovider_auth_oauth_and_more",
),
]
operations = [
migrations.AlterField(
model_name="scimprovider",
name="compatibility_mode",
field=models.CharField(
choices=[
("default", "Default"),
("aws", "AWS"),
("slack", "Slack"),
("sfdc", "Salesforce"),
],
default="default",
help_text="Alter authentik behavior for vendor-specific SCIM implementations.",
max_length=30,
verbose_name="SCIM Compatibility Mode",
),
),
]

View File

@@ -8,17 +8,12 @@ from django.db.models import QuerySet
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from dramatiq.actor import Actor
from requests.auth import AuthBase
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 SerializerModel
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.providers.scim.clients.auth import SCIMTokenAuth
LOGGER = get_logger()
class SCIMProviderUser(SerializerModel):
@@ -65,20 +60,12 @@ class SCIMProviderGroup(SerializerModel):
return f"SCIM Provider Group {self.group_id} to {self.provider_id}"
class SCIMAuthenticationMode(models.TextChoices):
"""SCIM authentication modes"""
TOKEN = "token", _("Token")
OAUTH = "oauth", _("OAuth")
class SCIMCompatibilityMode(models.TextChoices):
"""SCIM compatibility mode"""
DEFAULT = "default", _("Default")
AWS = "aws", _("AWS")
SLACK = "slack", _("Slack")
SALESFORCE = "sfdc", _("Salesforce")
class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
@@ -91,26 +78,7 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
)
url = models.TextField(help_text=_("Base URL to SCIM requests, usually ends in /v2"))
auth_mode = models.TextField(
choices=SCIMAuthenticationMode.choices, default=SCIMAuthenticationMode.TOKEN
)
token = models.TextField(help_text=_("Authentication token"), blank=True)
auth_oauth = models.ForeignKey(
"authentik_sources_oauth.OAuthSource",
on_delete=models.SET_DEFAULT,
default=None,
null=True,
help_text=_("OAuth Source used for authentication"),
)
auth_oauth_params = models.JSONField(
blank=True, default=dict, help_text=_("Additional OAuth parameters, such as grant_type")
)
auth_oauth_user = models.ForeignKey(
"authentik_core.User", on_delete=models.CASCADE, default=None, null=True
)
token = models.TextField(help_text=_("Authentication token"))
verify_certificates = models.BooleanField(default=True)
property_mappings_group = models.ManyToManyField(
@@ -128,16 +96,6 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
help_text=_("Alter authentik behavior for vendor-specific SCIM implementations."),
)
def scim_auth(self) -> AuthBase:
if self.auth_mode == SCIMAuthenticationMode.OAUTH:
try:
from authentik.enterprise.providers.scim.auth_oauth2 import SCIMOAuthAuth
return SCIMOAuthAuth(self)
except ImportError:
LOGGER.warning("Failed to import SCIM OAuth Client")
return SCIMTokenAuth(self)
@property
def icon_url(self) -> str | None:
return static("authentik/sources/scim.png")

View File

@@ -16,7 +16,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import Group, User, UserTypes
from authentik.policies.event_matcher.models import model_choices
@@ -38,15 +38,15 @@ class UserObjectPermissionSerializer(ModelSerializer):
fields = ["id", "codename", "model", "app_label", "object_pk", "name"]
class UserAssignedObjectPermissionSerializer(PartialUserSerializer):
class UserAssignedObjectPermissionSerializer(GroupMemberSerializer):
"""Users assigned object permission serializer"""
permissions = UserObjectPermissionSerializer(many=True, source="userobjectpermission_set")
is_superuser = BooleanField()
class Meta:
model = PartialUserSerializer.Meta.model
fields = PartialUserSerializer.Meta.fields + ["permissions", "is_superuser"]
model = GroupMemberSerializer.Meta.model
fields = GroupMemberSerializer.Meta.fields + ["permissions", "is_superuser"]
class UserAssignedPermissionFilter(FilterSet):

View File

@@ -1,70 +0,0 @@
"""InitialPermissions middleware"""
from collections.abc import Callable
from contextvars import ContextVar
from functools import partial
from django.db.models import Model
from django.db.models.signals import post_save
from django.http import HttpRequest, HttpResponse
from authentik.core.models import User
from authentik.rbac.permissions import assign_initial_permissions
_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_initial_permissions_request", default=None)
class InitialPermissionsMiddleware:
"""Register a handler for duration of request-response that assigns InitialPermissions"""
get_response: Callable[[HttpRequest], HttpResponse]
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def get_uid(self, request_id: str) -> str:
return f"InitialPermissionMiddleware-{request_id}"
def connect(self, request: HttpRequest):
if not hasattr(request, "request_id"):
return
post_save.connect(
partial(self.post_save_handler, request=request),
dispatch_uid=self.get_uid(request.request_id),
weak=False,
)
def disconnect(self, request: HttpRequest):
if not hasattr(request, "request_id"):
return
post_save.disconnect(dispatch_uid=self.get_uid(request.request_id))
def __call__(self, request: HttpRequest) -> HttpResponse:
_CTX_REQUEST.set(request)
self.connect(request)
response = self.get_response(request)
self.disconnect(request)
_CTX_REQUEST.set(None)
return response
def process_exception(self, request: HttpRequest, exception: Exception):
self.disconnect(request)
def post_save_handler(
self,
request: HttpRequest,
instance: Model,
created: bool,
**_,
):
if not created:
return
current_request = _CTX_REQUEST.get()
if current_request is None or request.request_id != current_request.request_id:
return
user: User = request.user
if not user or user.is_anonymous:
return
assign_initial_permissions(user, instance)

View File

@@ -5,12 +5,9 @@ from django.db.models import Model
from guardian.shortcuts import assign_perm
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from rest_framework.request import Request
from structlog.stdlib import get_logger
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode
LOGGER = get_logger()
class ObjectPermissions(DjangoObjectPermissions):
"""RBAC Permissions"""
@@ -74,10 +71,4 @@ def assign_initial_permissions(user, instance: Model):
if initial_permissions.mode == InitialPermissionsMode.USER
else initial_permissions.role.group
)
LOGGER.debug(
"Adding initial permission",
initial_permission=permission,
subject=assign_to,
object=instance,
)
assign_perm(permission, assign_to, instance)

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